@kiva/kv-components 3.105.2 → 3.106.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.
- package/CHANGELOG.md +19 -0
- package/package.json +2 -2
- package/utils/carousels.js +190 -0
- package/vue/KvCarousel.vue +33 -182
- package/vue/KvMap.vue +2 -0
- package/vue/KvVerticalCarousel.vue +156 -0
- package/vue/stories/KvVerticalCarousel.stories.js +168 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [3.106.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.105.3...@kiva/kv-components@3.106.0) (2024-11-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* basic vertical carousel ([6a40563](https://github.com/kiva/kv-ui-elements/commit/6a405637ee4186d3cb5a7dbf71f87d8a04d46d86))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [3.105.3](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.105.2...@kiva/kv-components@3.105.3) (2024-10-28)
|
|
18
|
+
|
|
19
|
+
**Note:** Version bump only for package @kiva/kv-components
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## [3.105.2](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.105.1...@kiva/kv-components@3.105.2) (2024-10-25)
|
|
7
26
|
|
|
8
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kiva/kv-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.106.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
"optional": true
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
-
"gitHead": "
|
|
86
|
+
"gitHead": "c195fd4d6bf8055879846dc5821873e9bc336f1c"
|
|
87
87
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
onMounted,
|
|
5
|
+
onUnmounted,
|
|
6
|
+
ref,
|
|
7
|
+
toRefs,
|
|
8
|
+
nextTick,
|
|
9
|
+
} from 'vue-demi';
|
|
10
|
+
import EmblaCarousel from 'embla-carousel';
|
|
11
|
+
import { throttle } from './throttle';
|
|
12
|
+
|
|
13
|
+
export function carouselUtil(props, { emit, slots }, extraEmblaOptions) {
|
|
14
|
+
const {
|
|
15
|
+
emblaOptions,
|
|
16
|
+
slidesToScroll,
|
|
17
|
+
} = toRefs(props);
|
|
18
|
+
const rootEl = ref(null);
|
|
19
|
+
const embla = ref(null);
|
|
20
|
+
const slides = ref([]);
|
|
21
|
+
const startIndex = emblaOptions.value?.startIndex ?? 0;
|
|
22
|
+
const currentIndex = ref(startIndex);
|
|
23
|
+
// The indicator count may differ from the slide count when multiple slides are in view
|
|
24
|
+
const slideIndicatorCount = ref(0);
|
|
25
|
+
|
|
26
|
+
const componentSlotKeys = computed(() => {
|
|
27
|
+
const keys = Object.keys(slots);
|
|
28
|
+
return keys;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const nextIndex = computed(() => {
|
|
32
|
+
const nextSlideIndex = currentIndex.value + 1;
|
|
33
|
+
if (nextSlideIndex < slides.value.length) {
|
|
34
|
+
return nextSlideIndex;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const previousIndex = computed(() => {
|
|
40
|
+
const previousSlideIndex = currentIndex.value - 1;
|
|
41
|
+
if (previousSlideIndex >= 0) {
|
|
42
|
+
return previousSlideIndex;
|
|
43
|
+
}
|
|
44
|
+
return slides.value.length - 1;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Jump to a specific slide index
|
|
49
|
+
*
|
|
50
|
+
* @param {Number} num Index of slide to show
|
|
51
|
+
* @public This is a public method
|
|
52
|
+
*/
|
|
53
|
+
const goToSlide = (index) => {
|
|
54
|
+
embla.value.scrollTo(index);
|
|
55
|
+
};
|
|
56
|
+
const handleUserInteraction = async (index, interactionType) => {
|
|
57
|
+
if (index !== null && typeof index !== 'undefined') {
|
|
58
|
+
await nextTick(); // wait for embla.
|
|
59
|
+
goToSlide(index);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Fires when the user interacts with the carousel.
|
|
63
|
+
* Contains the interaction type (swipe-left, click-left-arrow, etc.)
|
|
64
|
+
* @event interact-carousel
|
|
65
|
+
* @type {Event}
|
|
66
|
+
*/
|
|
67
|
+
emit('interact-carousel', interactionType);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns number of slides in the carousel
|
|
72
|
+
*
|
|
73
|
+
* @returns {Number}
|
|
74
|
+
*/
|
|
75
|
+
const slideIndicatorListLength = () => {
|
|
76
|
+
const indicator = embla.value ? embla.value.scrollSnapList().length : 0;
|
|
77
|
+
slideIndicatorCount.value = indicator;
|
|
78
|
+
return indicator;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reinitialize the carousel.
|
|
83
|
+
* Used after adding slides dynamically.
|
|
84
|
+
*
|
|
85
|
+
* @public This is a public method
|
|
86
|
+
*/
|
|
87
|
+
const reInitVisible = () => {
|
|
88
|
+
const slidesInView = embla.value.slidesInView(true).length;
|
|
89
|
+
if (slidesInView) {
|
|
90
|
+
embla.value.reInit({
|
|
91
|
+
slidesToScroll: slidesInView,
|
|
92
|
+
inViewThreshold: 0.9,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const reInit = () => {
|
|
97
|
+
embla.value.reInit();
|
|
98
|
+
if (slidesToScroll.value === 'visible') {
|
|
99
|
+
reInitVisible();
|
|
100
|
+
}
|
|
101
|
+
slides.value = embla.value.slideNodes();
|
|
102
|
+
slideIndicatorListLength();
|
|
103
|
+
};
|
|
104
|
+
const onCarouselContainerClick = (e) => {
|
|
105
|
+
// If we're dragging, block click handlers within slides
|
|
106
|
+
if (embla.value && !embla.value.clickAllowed()) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* If the slide is not completely in view in the carousel
|
|
113
|
+
* it should be aria-hidden
|
|
114
|
+
*
|
|
115
|
+
* @param {Number} index The current index of the slide
|
|
116
|
+
* @returns {Boolean}
|
|
117
|
+
*/
|
|
118
|
+
const isAriaHidden = (index) => {
|
|
119
|
+
if (embla.value) {
|
|
120
|
+
return !embla.value.slidesInView(true).includes(index);
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
onMounted(async () => {
|
|
126
|
+
embla.value = EmblaCarousel(rootEl.value, {
|
|
127
|
+
loop: true,
|
|
128
|
+
containScroll: 'trimSnaps',
|
|
129
|
+
inViewThreshold: 0.9,
|
|
130
|
+
align: 'start',
|
|
131
|
+
...extraEmblaOptions,
|
|
132
|
+
...emblaOptions.value,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (slidesToScroll.value === 'visible') {
|
|
136
|
+
reInitVisible();
|
|
137
|
+
|
|
138
|
+
embla.value.on(
|
|
139
|
+
'resize',
|
|
140
|
+
throttle(() => {
|
|
141
|
+
embla.value.reInit({
|
|
142
|
+
slidesToScroll: embla.value.slidesInView(true).length || 'auto',
|
|
143
|
+
inViewThreshold: 0.9,
|
|
144
|
+
});
|
|
145
|
+
slides.value = embla.value.slideNodes();
|
|
146
|
+
slideIndicatorListLength();
|
|
147
|
+
}, 250),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// get slide components
|
|
152
|
+
slides.value = embla.value.slideNodes();
|
|
153
|
+
slideIndicatorListLength();
|
|
154
|
+
|
|
155
|
+
embla?.value?.on('select', () => {
|
|
156
|
+
currentIndex.value = embla.value.selectedScrollSnap();
|
|
157
|
+
/**
|
|
158
|
+
* The index of the slide that the carousel has changed to
|
|
159
|
+
* @event change
|
|
160
|
+
* @type {Event}
|
|
161
|
+
*/
|
|
162
|
+
nextTick(() => {
|
|
163
|
+
emit('change', currentIndex);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
onUnmounted(async () => {
|
|
169
|
+
embla?.value?.off('select');
|
|
170
|
+
embla?.value?.destroy();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
rootEl,
|
|
175
|
+
embla,
|
|
176
|
+
slides,
|
|
177
|
+
currentIndex,
|
|
178
|
+
componentSlotKeys,
|
|
179
|
+
nextIndex,
|
|
180
|
+
previousIndex,
|
|
181
|
+
slideIndicatorCount,
|
|
182
|
+
handleUserInteraction,
|
|
183
|
+
goToSlide,
|
|
184
|
+
reInit,
|
|
185
|
+
reInitVisible,
|
|
186
|
+
onCarouselContainerClick,
|
|
187
|
+
isAriaHidden,
|
|
188
|
+
slideIndicatorListLength,
|
|
189
|
+
};
|
|
190
|
+
}
|
package/vue/KvCarousel.vue
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
:aria-current="currentIndex === index ? 'true' : 'false'"
|
|
24
24
|
:aria-hidden="isAriaHidden(index)? 'true' : 'false'"
|
|
25
25
|
:tab-index="isAriaHidden(index) ? '-1' : false"
|
|
26
|
-
:class="{ 'tw-w-full': !multipleSlidesVisible || slideMaxWidth, '
|
|
26
|
+
:class="{ 'tw-w-full': !multipleSlidesVisible || slideMaxWidth, 'circle-slide': inCircle }"
|
|
27
27
|
:style="slideMaxWidth ? `max-width:${slideMaxWidth}` :''"
|
|
28
28
|
>
|
|
29
29
|
<slot
|
|
@@ -146,22 +146,13 @@
|
|
|
146
146
|
</template>
|
|
147
147
|
|
|
148
148
|
<script>
|
|
149
|
-
import {
|
|
150
|
-
computed,
|
|
151
|
-
onMounted,
|
|
152
|
-
onUnmounted,
|
|
153
|
-
ref,
|
|
154
|
-
toRefs,
|
|
155
|
-
nextTick,
|
|
156
|
-
} from 'vue-demi';
|
|
157
|
-
import EmblaCarousel from 'embla-carousel';
|
|
158
149
|
import {
|
|
159
150
|
mdiChevronLeft,
|
|
160
151
|
mdiChevronRight,
|
|
161
152
|
mdiArrowLeft,
|
|
162
153
|
mdiArrowRight,
|
|
163
154
|
} from '@mdi/js';
|
|
164
|
-
import {
|
|
155
|
+
import { carouselUtil } from '../utils/carousels';
|
|
165
156
|
|
|
166
157
|
import KvMaterialIcon from './KvMaterialIcon.vue';
|
|
167
158
|
|
|
@@ -236,183 +227,43 @@ export default {
|
|
|
236
227
|
],
|
|
237
228
|
setup(props, { emit, slots }) {
|
|
238
229
|
const {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const nextIndex = computed(() => {
|
|
256
|
-
const nextSlideIndex = currentIndex.value + 1;
|
|
257
|
-
if (nextSlideIndex < slides.value.length) {
|
|
258
|
-
return nextSlideIndex;
|
|
259
|
-
}
|
|
260
|
-
return 0;
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
const previousIndex = computed(() => {
|
|
264
|
-
const previousSlideIndex = currentIndex.value - 1;
|
|
265
|
-
if (previousSlideIndex >= 0) {
|
|
266
|
-
return previousSlideIndex;
|
|
267
|
-
}
|
|
268
|
-
return slides.value.length - 1;
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Jump to a specific slide index
|
|
273
|
-
*
|
|
274
|
-
* @param {Number} num Index of slide to show
|
|
275
|
-
* @public This is a public method
|
|
276
|
-
*/
|
|
277
|
-
const goToSlide = (index) => {
|
|
278
|
-
embla.value.scrollTo(index);
|
|
279
|
-
};
|
|
280
|
-
const handleUserInteraction = async (index, interactionType) => {
|
|
281
|
-
if (index !== null && typeof index !== 'undefined') {
|
|
282
|
-
await nextTick(); // wait for embla.
|
|
283
|
-
goToSlide(index);
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* Fires when the user interacts with the carousel.
|
|
287
|
-
* Contains the interaction type (swipe-left, click-left-arrow, etc.)
|
|
288
|
-
* @event interact-carousel
|
|
289
|
-
* @type {Event}
|
|
290
|
-
*/
|
|
291
|
-
emit('interact-carousel', interactionType);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Returns number of slides in the carousel
|
|
296
|
-
*
|
|
297
|
-
* @returns {Number}
|
|
298
|
-
*/
|
|
299
|
-
const slideIndicatorListLength = () => {
|
|
300
|
-
const indicator = embla.value ? embla.value.scrollSnapList().length : 0;
|
|
301
|
-
slideIndicatorCount.value = indicator;
|
|
302
|
-
return indicator;
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Reinitialize the carousel.
|
|
307
|
-
* Used after adding slides dynamically.
|
|
308
|
-
*
|
|
309
|
-
* @public This is a public method
|
|
310
|
-
*/
|
|
311
|
-
const reInitVisible = () => {
|
|
312
|
-
const slidesInView = embla.value.slidesInView(true).length;
|
|
313
|
-
if (slidesInView) {
|
|
314
|
-
embla.value.reInit({
|
|
315
|
-
slidesToScroll: slidesInView,
|
|
316
|
-
inViewThreshold: 0.9,
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
const reInit = () => {
|
|
321
|
-
embla.value.reInit();
|
|
322
|
-
if (slidesToScroll.value === 'visible') {
|
|
323
|
-
reInitVisible();
|
|
324
|
-
}
|
|
325
|
-
slides.value = embla.value.slideNodes();
|
|
326
|
-
slideIndicatorListLength();
|
|
327
|
-
};
|
|
328
|
-
const onCarouselContainerClick = (e) => {
|
|
329
|
-
// If we're dragging, block click handlers within slides
|
|
330
|
-
if (embla.value && !embla.value.clickAllowed()) {
|
|
331
|
-
e.preventDefault();
|
|
332
|
-
e.stopPropagation();
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
/**
|
|
336
|
-
* If the slide is not completely in view in the carousel
|
|
337
|
-
* it should be aria-hidden
|
|
338
|
-
*
|
|
339
|
-
* @param {Number} index The current index of the slide
|
|
340
|
-
* @returns {Boolean}
|
|
341
|
-
*/
|
|
342
|
-
const isAriaHidden = (index) => {
|
|
343
|
-
if (embla.value) {
|
|
344
|
-
return !embla.value.slidesInView(true).includes(index);
|
|
345
|
-
}
|
|
346
|
-
return false;
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
onMounted(async () => {
|
|
350
|
-
embla.value = EmblaCarousel(rootEl.value, {
|
|
351
|
-
loop: true,
|
|
352
|
-
containScroll: 'trimSnaps',
|
|
353
|
-
inViewThreshold: 0.9,
|
|
354
|
-
align: 'start',
|
|
355
|
-
...emblaOptions.value,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
if (slidesToScroll.value === 'visible') {
|
|
359
|
-
reInitVisible();
|
|
360
|
-
|
|
361
|
-
embla.value.on(
|
|
362
|
-
'resize',
|
|
363
|
-
throttle(() => {
|
|
364
|
-
embla.value.reInit({
|
|
365
|
-
slidesToScroll: embla.value.slidesInView(true).length || 'auto',
|
|
366
|
-
inViewThreshold: 0.9,
|
|
367
|
-
});
|
|
368
|
-
slides.value = embla.value.slideNodes();
|
|
369
|
-
slideIndicatorListLength();
|
|
370
|
-
}, 250),
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// get slide components
|
|
375
|
-
slides.value = embla.value.slideNodes();
|
|
376
|
-
slideIndicatorListLength();
|
|
377
|
-
|
|
378
|
-
embla?.value?.on('select', () => {
|
|
379
|
-
currentIndex.value = embla.value.selectedScrollSnap();
|
|
380
|
-
/**
|
|
381
|
-
* The index of the slide that the carousel has changed to
|
|
382
|
-
* @event change
|
|
383
|
-
* @type {Event}
|
|
384
|
-
*/
|
|
385
|
-
nextTick(() => {
|
|
386
|
-
emit('change', currentIndex);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
onUnmounted(async () => {
|
|
392
|
-
embla?.value?.off('select');
|
|
393
|
-
embla?.value?.destroy();
|
|
394
|
-
});
|
|
230
|
+
componentSlotKeys,
|
|
231
|
+
currentIndex,
|
|
232
|
+
embla,
|
|
233
|
+
goToSlide,
|
|
234
|
+
handleUserInteraction,
|
|
235
|
+
isAriaHidden,
|
|
236
|
+
nextIndex,
|
|
237
|
+
onCarouselContainerClick,
|
|
238
|
+
previousIndex,
|
|
239
|
+
reInit,
|
|
240
|
+
reInitVisible,
|
|
241
|
+
rootEl,
|
|
242
|
+
slideIndicatorCount,
|
|
243
|
+
slideIndicatorListLength,
|
|
244
|
+
slides,
|
|
245
|
+
} = carouselUtil(props, { emit, slots });
|
|
395
246
|
|
|
396
247
|
return {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
248
|
+
componentSlotKeys,
|
|
249
|
+
currentIndex,
|
|
250
|
+
embla,
|
|
251
|
+
goToSlide,
|
|
252
|
+
handleUserInteraction,
|
|
253
|
+
isAriaHidden,
|
|
400
254
|
mdiArrowLeft,
|
|
401
255
|
mdiArrowRight,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
currentIndex,
|
|
405
|
-
componentSlotKeys,
|
|
256
|
+
mdiChevronLeft,
|
|
257
|
+
mdiChevronRight,
|
|
406
258
|
nextIndex,
|
|
259
|
+
onCarouselContainerClick,
|
|
407
260
|
previousIndex,
|
|
408
|
-
slideIndicatorCount,
|
|
409
|
-
handleUserInteraction,
|
|
410
|
-
goToSlide,
|
|
411
261
|
reInit,
|
|
412
262
|
reInitVisible,
|
|
413
|
-
|
|
414
|
-
|
|
263
|
+
rootEl,
|
|
264
|
+
slideIndicatorCount,
|
|
415
265
|
slideIndicatorListLength,
|
|
266
|
+
slides,
|
|
416
267
|
};
|
|
417
268
|
},
|
|
418
269
|
};
|
|
@@ -425,17 +276,17 @@ export default {
|
|
|
425
276
|
}
|
|
426
277
|
}
|
|
427
278
|
|
|
428
|
-
.
|
|
279
|
+
.circle-slide {
|
|
429
280
|
width: auto;
|
|
430
281
|
}
|
|
431
282
|
|
|
432
|
-
.
|
|
283
|
+
.circle-slide.is-selected {
|
|
433
284
|
opacity: 1;
|
|
434
285
|
transform: scale(1.2);
|
|
435
286
|
max-width: 300px;
|
|
436
287
|
}
|
|
437
288
|
|
|
438
|
-
.
|
|
289
|
+
.circle-slide:not(.is-selected) {
|
|
439
290
|
opacity: 0.5;
|
|
440
291
|
transform: scale(0.7);
|
|
441
292
|
}
|
package/vue/KvMap.vue
CHANGED
|
@@ -203,6 +203,8 @@ export default {
|
|
|
203
203
|
},
|
|
204
204
|
async mounted() {
|
|
205
205
|
if (this.countriesData) {
|
|
206
|
+
// current source data is from https://geojson.xyz/ under "admin 0 countries"
|
|
207
|
+
// https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson
|
|
206
208
|
this.countriesBorders = await import('../data/ne_110m_admin_0_countries.json');
|
|
207
209
|
}
|
|
208
210
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<section
|
|
4
|
+
ref="rootEl"
|
|
5
|
+
aria-label="carousel"
|
|
6
|
+
class="kv-carousel tw-overflow-hidden tw-relative"
|
|
7
|
+
>
|
|
8
|
+
<!-- Carousel Content -->
|
|
9
|
+
<div
|
|
10
|
+
class="tw-flex tw-flex-col tw-gap-y-1"
|
|
11
|
+
:style="`height: ${heightStyle}`"
|
|
12
|
+
@click.capture="onCarouselContainerClick"
|
|
13
|
+
>
|
|
14
|
+
<div
|
|
15
|
+
v-for="(slotName, index) in componentSlotKeys"
|
|
16
|
+
:key="index"
|
|
17
|
+
role="group"
|
|
18
|
+
:aria-label="`slide ${index + 1} of ${componentSlotKeys.length}`"
|
|
19
|
+
:aria-current="currentIndex === index ? 'true' : 'false'"
|
|
20
|
+
:aria-hidden="isAriaHidden(index)? 'true' : 'false'"
|
|
21
|
+
:tab-index="isAriaHidden(index) ? '-1' : false"
|
|
22
|
+
>
|
|
23
|
+
<slot
|
|
24
|
+
:name="slotName"
|
|
25
|
+
></slot>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
<!-- Carousel Controls -->
|
|
30
|
+
<div
|
|
31
|
+
class="kv-carousel__controls tw-flex
|
|
32
|
+
tw-justify-between md:tw-justify-center tw-items-center tw-gap-2
|
|
33
|
+
tw-mt-2 tw-w-full"
|
|
34
|
+
>
|
|
35
|
+
<button
|
|
36
|
+
class="tw-text-primary
|
|
37
|
+
tw-rounded-full
|
|
38
|
+
tw-border-2 tw-border-primary
|
|
39
|
+
tw-h-4 tw-w-4
|
|
40
|
+
tw-flex tw-items-center tw-justify-center
|
|
41
|
+
disabled:tw-opacity-low disabled:tw-cursor-default"
|
|
42
|
+
:disabled="embla && !embla.canScrollPrev()"
|
|
43
|
+
@click="handleUserInteraction(previousIndex, 'click-left-arrow')"
|
|
44
|
+
>
|
|
45
|
+
<kv-material-icon
|
|
46
|
+
class="tw-w-4"
|
|
47
|
+
:icon="mdiChevronUp"
|
|
48
|
+
/>
|
|
49
|
+
<span class="tw-sr-only">Show previous slide</span>
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
class="tw-text-primary
|
|
53
|
+
tw-rounded-full
|
|
54
|
+
tw-border-2 tw-border-primary
|
|
55
|
+
tw-h-4 tw-w-4
|
|
56
|
+
tw-flex tw-items-center tw-justify-center
|
|
57
|
+
disabled:tw-opacity-low disabled:tw-cursor-default"
|
|
58
|
+
:disabled="embla && !embla.canScrollNext()"
|
|
59
|
+
@click="handleUserInteraction(nextIndex, 'click-right-arrow')"
|
|
60
|
+
>
|
|
61
|
+
<kv-material-icon
|
|
62
|
+
class="tw-w-4"
|
|
63
|
+
:icon="mdiChevronDown"
|
|
64
|
+
/>
|
|
65
|
+
<span class="tw-sr-only">Show next slide</span>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import {
|
|
73
|
+
mdiChevronUp,
|
|
74
|
+
mdiChevronDown,
|
|
75
|
+
} from '@mdi/js';
|
|
76
|
+
import { carouselUtil } from '../utils/carousels';
|
|
77
|
+
import KvMaterialIcon from './KvMaterialIcon.vue';
|
|
78
|
+
|
|
79
|
+
export default {
|
|
80
|
+
components: {
|
|
81
|
+
KvMaterialIcon,
|
|
82
|
+
},
|
|
83
|
+
props: {
|
|
84
|
+
/**
|
|
85
|
+
* Height style declaration of the vertical carousel.
|
|
86
|
+
* */
|
|
87
|
+
heightStyle: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: '400px;',
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* Options for the embla carousel - // https://davidcetinkaya.github.io/embla-carousel/api#options
|
|
93
|
+
* */
|
|
94
|
+
emblaOptions: {
|
|
95
|
+
type: Object,
|
|
96
|
+
default() {
|
|
97
|
+
return {};
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* The type of logic to implement when deciding how many slides
|
|
102
|
+
* to scroll when pressing the next/prev button
|
|
103
|
+
* `visible, auto`
|
|
104
|
+
* */
|
|
105
|
+
slidesToScroll: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: 'auto',
|
|
108
|
+
validator: (value) => ['visible', 'auto'].indexOf(value) !== -1,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
emits: [
|
|
112
|
+
'change',
|
|
113
|
+
'interact-carousel',
|
|
114
|
+
],
|
|
115
|
+
setup(props, { emit, slots }) {
|
|
116
|
+
const {
|
|
117
|
+
componentSlotKeys,
|
|
118
|
+
currentIndex,
|
|
119
|
+
embla,
|
|
120
|
+
goToSlide,
|
|
121
|
+
handleUserInteraction,
|
|
122
|
+
isAriaHidden,
|
|
123
|
+
nextIndex,
|
|
124
|
+
onCarouselContainerClick,
|
|
125
|
+
previousIndex,
|
|
126
|
+
reInit,
|
|
127
|
+
reInitVisible,
|
|
128
|
+
rootEl,
|
|
129
|
+
slideIndicatorCount,
|
|
130
|
+
slides,
|
|
131
|
+
} = carouselUtil(props, { emit, slots }, { axis: 'y' });
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
componentSlotKeys,
|
|
135
|
+
currentIndex,
|
|
136
|
+
embla,
|
|
137
|
+
goToSlide,
|
|
138
|
+
handleUserInteraction,
|
|
139
|
+
isAriaHidden,
|
|
140
|
+
mdiChevronDown,
|
|
141
|
+
mdiChevronUp,
|
|
142
|
+
nextIndex,
|
|
143
|
+
onCarouselContainerClick,
|
|
144
|
+
previousIndex,
|
|
145
|
+
reInit,
|
|
146
|
+
reInitVisible,
|
|
147
|
+
rootEl,
|
|
148
|
+
slideIndicatorCount,
|
|
149
|
+
slides,
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<style scoped>
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import KvVerticalCarousel from '../KvVerticalCarousel.vue';
|
|
2
|
+
import KvButton from '../KvButton.vue';
|
|
3
|
+
|
|
4
|
+
const randomHexColor = (index) => {
|
|
5
|
+
const defaultColor = '96d4b3';
|
|
6
|
+
const colorArray = [
|
|
7
|
+
'D5573B',
|
|
8
|
+
'885053',
|
|
9
|
+
'777DA7',
|
|
10
|
+
'94C9A9',
|
|
11
|
+
'C6ECAE',
|
|
12
|
+
'C490D1',
|
|
13
|
+
'A0D2DB',
|
|
14
|
+
'7D8CC4',
|
|
15
|
+
'726DA8',
|
|
16
|
+
];
|
|
17
|
+
return colorArray?.[index] || defaultColor;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// This is not an actual loan card template, just a vague
|
|
21
|
+
// approximation to make testing in the stories nicer
|
|
22
|
+
const generateLoanCardTemplate = (index) => {
|
|
23
|
+
const amounts = [
|
|
24
|
+
'100',
|
|
25
|
+
'2,255',
|
|
26
|
+
'50',
|
|
27
|
+
'41,900',
|
|
28
|
+
];
|
|
29
|
+
const cardCopy = [
|
|
30
|
+
'A loan of $5,450 helps a member to buy flour, eggs, lard, sugar, sweets and other...',
|
|
31
|
+
'A loan of $1,125 helps to face the financial problem of covering tuition fees.',
|
|
32
|
+
'A loan of $450 helps to purchase store goods for resale.',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
return `
|
|
36
|
+
<div style="width: 336px">
|
|
37
|
+
<img src="https://placehold.co/336x252/${randomHexColor(index)}/000000" class="tw-w-full tw-rounded tw-mb-2">
|
|
38
|
+
<h3>Card Title</h3>
|
|
39
|
+
<h4 class="tw-my-1">$${amounts?.[index] || amounts?.[1]} to go</h4>
|
|
40
|
+
<p class="tw-mt-1 tw-mb-9">${cardCopy?.[index] || cardCopy?.[1]}</p>
|
|
41
|
+
<kv-button
|
|
42
|
+
variant="primary"
|
|
43
|
+
>
|
|
44
|
+
Read more
|
|
45
|
+
</kv-button>
|
|
46
|
+
</div>`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const defaultCarouselSlides = `
|
|
50
|
+
<template #slide1>
|
|
51
|
+
<img src="https://placehold.co/400x150/${randomHexColor(1)}/000000">
|
|
52
|
+
</template>
|
|
53
|
+
<template #slide2>
|
|
54
|
+
<img src="https://placehold.co/400x150/${randomHexColor(2)}/000000">
|
|
55
|
+
</template>
|
|
56
|
+
<template #slide3>
|
|
57
|
+
<img src="https://placehold.co/400x150/${randomHexColor(3)}/000000">
|
|
58
|
+
</template>
|
|
59
|
+
<template #slide4>
|
|
60
|
+
<img src="https://placehold.co/400x150/${randomHexColor(4)}/000000">
|
|
61
|
+
</template>
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
export default {
|
|
65
|
+
title: 'KvVerticalCarousel',
|
|
66
|
+
component: KvVerticalCarousel,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const Default = () => ({
|
|
70
|
+
components: {
|
|
71
|
+
KvVerticalCarousel,
|
|
72
|
+
},
|
|
73
|
+
template: `
|
|
74
|
+
<div style="width: 400px;">
|
|
75
|
+
<kv-vertical-carousel height-style="310px" class="tw-w-full">
|
|
76
|
+
${defaultCarouselSlides}
|
|
77
|
+
</kv-vertical-carousel>
|
|
78
|
+
</div>
|
|
79
|
+
`,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const LoopFalse = () => ({
|
|
83
|
+
components: {
|
|
84
|
+
KvVerticalCarousel,
|
|
85
|
+
},
|
|
86
|
+
template: `
|
|
87
|
+
<div style="width: 400px;">
|
|
88
|
+
<kv-vertical-carousel height-style="400px" class="tw-w-full">
|
|
89
|
+
${defaultCarouselSlides}
|
|
90
|
+
</kv-vertical-carousel>
|
|
91
|
+
</div>
|
|
92
|
+
`,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const MultipleLoanCards = () => ({
|
|
96
|
+
components: {
|
|
97
|
+
KvVerticalCarousel,
|
|
98
|
+
KvButton,
|
|
99
|
+
},
|
|
100
|
+
template: `
|
|
101
|
+
<kv-vertical-carousel
|
|
102
|
+
:embla-options="{ loop: false }"
|
|
103
|
+
:multiple-slides-visible="true"
|
|
104
|
+
style="max-width: 600px;"
|
|
105
|
+
height-style="510px"
|
|
106
|
+
class="tw-w-full"
|
|
107
|
+
>
|
|
108
|
+
<template #slide1>
|
|
109
|
+
${generateLoanCardTemplate(1)}
|
|
110
|
+
</template>
|
|
111
|
+
<template #slide2>
|
|
112
|
+
${generateLoanCardTemplate(2)}
|
|
113
|
+
</template>
|
|
114
|
+
<template #slide3>
|
|
115
|
+
${generateLoanCardTemplate(3)}
|
|
116
|
+
</template>
|
|
117
|
+
<template #slide4>
|
|
118
|
+
${generateLoanCardTemplate(4)}
|
|
119
|
+
</template>
|
|
120
|
+
<template #slide5>
|
|
121
|
+
${generateLoanCardTemplate(5)}
|
|
122
|
+
</template>
|
|
123
|
+
</kv-vertical-carousel>
|
|
124
|
+
`,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const CustomStartIndex = () => ({
|
|
128
|
+
components: {
|
|
129
|
+
KvVerticalCarousel,
|
|
130
|
+
KvButton,
|
|
131
|
+
},
|
|
132
|
+
template: `
|
|
133
|
+
<kv-vertical-carousel
|
|
134
|
+
:embla-options="{ loop: false, align: 'center', startIndex: 1 }"
|
|
135
|
+
:multiple-slides-visible="true"
|
|
136
|
+
slides-to-scroll="visible"
|
|
137
|
+
style="max-width: 1072px;"
|
|
138
|
+
>
|
|
139
|
+
<template #slide1>
|
|
140
|
+
${generateLoanCardTemplate(1)}
|
|
141
|
+
</template>
|
|
142
|
+
<template #slide2>
|
|
143
|
+
${generateLoanCardTemplate(2)}
|
|
144
|
+
</template>
|
|
145
|
+
<template #slide3>
|
|
146
|
+
${generateLoanCardTemplate(3)}
|
|
147
|
+
</template>
|
|
148
|
+
<template #slide4>
|
|
149
|
+
${generateLoanCardTemplate(4)}
|
|
150
|
+
</template>
|
|
151
|
+
<template #slide5>
|
|
152
|
+
${generateLoanCardTemplate(5)}
|
|
153
|
+
</template>
|
|
154
|
+
<template #slide6>
|
|
155
|
+
${generateLoanCardTemplate(6)}
|
|
156
|
+
</template>
|
|
157
|
+
<template #slide7>
|
|
158
|
+
${generateLoanCardTemplate(7)}
|
|
159
|
+
</template>
|
|
160
|
+
<template #slide8>
|
|
161
|
+
${generateLoanCardTemplate(8)}
|
|
162
|
+
</template>
|
|
163
|
+
<template #slide9>
|
|
164
|
+
${generateLoanCardTemplate(9)}
|
|
165
|
+
</template>
|
|
166
|
+
</kv-vertical-carousel>
|
|
167
|
+
`,
|
|
168
|
+
});
|