@kiva/kv-components 2.0.0 → 3.0.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/{.eslintrc.js → .eslintrc.cjs} +1 -1
- package/CHANGELOG.md +69 -0
- package/__mocks__/ResizeObserver.js +13 -0
- package/package.json +23 -10
- package/{postcss.config.js → postcss.config.cjs} +2 -2
- package/{tailwind.config.js → tailwind.config.cjs} +3 -3
- package/tests/unit/jest-setup.js +5 -0
- package/tests/unit/specs/components/KvButton.spec.js +38 -25
- package/tests/unit/specs/components/KvCarousel.spec.js +11 -0
- package/tests/unit/specs/components/KvCheckbox.spec.js +73 -14
- package/tests/unit/specs/components/KvLightbox.spec.js +14 -0
- package/tests/unit/specs/components/KvProgressBar.spec.js +11 -0
- package/tests/unit/specs/components/KvRadio.spec.js +94 -5
- package/tests/unit/specs/components/KvSelect.spec.js +113 -0
- package/tests/unit/specs/components/KvSwitch.spec.js +92 -33
- package/tests/unit/specs/components/KvTabPanel.spec.js +32 -0
- package/tests/unit/specs/components/KvTabs.spec.js +167 -0
- package/tests/unit/specs/components/KvTextInput.spec.js +86 -9
- package/tests/unit/specs/components/KvTextLink.spec.js +16 -24
- package/tests/unit/specs/components/KvToast.spec.js +11 -0
- package/tests/unit/utils/addVueRouter.js +24 -0
- package/utils/attrs.js +62 -0
- package/utils/{themeUtils.js → themeUtils.cjs} +0 -0
- package/vue/.storybook/{main.js → main.cjs} +13 -5
- package/vue/.storybook/preview.js +6 -1
- package/vue/KvButton.vue +75 -53
- package/vue/KvCarousel.vue +142 -106
- package/vue/KvCheckbox.vue +86 -60
- package/vue/KvContentfulImg.vue +45 -34
- package/vue/KvLightbox.vue +108 -69
- package/vue/KvProgressBar.vue +33 -19
- package/vue/KvRadio.vue +72 -41
- package/vue/KvSelect.vue +46 -20
- package/vue/KvSwitch.vue +55 -33
- package/vue/KvTab.vue +49 -21
- package/vue/KvTabPanel.vue +26 -6
- package/vue/KvTabs.vue +73 -55
- package/vue/KvTextInput.vue +71 -48
- package/vue/KvTextLink.vue +42 -20
- package/vue/KvThemeProvider.vue +1 -1
- package/vue/KvToast.vue +53 -37
- package/vue/stories/KvCheckbox.stories.js +5 -5
- package/vue/stories/KvSwitch.stories.js +2 -2
- package/vue/stories/KvTabs.stories.js +8 -8
- package/vue/stories/KvTextInput.stories.js +1 -1
- package/vue/stories/KvThemeProvider.stories.js +1 -1
- package/vue/stories/KvToast.stories.js +3 -2
- package/vue/stories/StyleguidePrimitives.stories.js +9 -9
- package/.babelrc +0 -16
- package/jest.config.js +0 -36
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { render } from '@testing-library/vue';
|
|
2
|
+
import { axe } from 'jest-axe';
|
|
3
|
+
import KvToast from '../../../../vue/KvToast.vue';
|
|
4
|
+
|
|
5
|
+
describe('KvToast', () => {
|
|
6
|
+
it('has no automated accessibility violations', async () => {
|
|
7
|
+
const { container } = render(KvToast);
|
|
8
|
+
const results = await axe(container);
|
|
9
|
+
expect(results).toHaveNoViolations();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { isVue3 } from 'vue-demi';
|
|
3
|
+
import * as VueRouter from 'vue-router';
|
|
4
|
+
|
|
5
|
+
export default function addVueRouter(testingLibraryOptions, vueRouterOptions) {
|
|
6
|
+
const opts = { ...testingLibraryOptions };
|
|
7
|
+
|
|
8
|
+
if (isVue3) {
|
|
9
|
+
// create opts.global.plugins array if it does not exist
|
|
10
|
+
opts.global = opts.global ?? {};
|
|
11
|
+
opts.global.plugins = opts.global.plugins ?? [];
|
|
12
|
+
|
|
13
|
+
// add Vue Router to plugins array
|
|
14
|
+
opts.global.plugins.push(VueRouter.createRouter(vueRouterOptions ?? {
|
|
15
|
+
history: VueRouter.createWebHashHistory(),
|
|
16
|
+
routes: [{ path: '/:path(.*)', component: {} }],
|
|
17
|
+
}));
|
|
18
|
+
} else {
|
|
19
|
+
const VueRouterDefault = VueRouter.default;
|
|
20
|
+
opts.routes = new VueRouterDefault(vueRouterOptions);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return opts;
|
|
24
|
+
}
|
package/utils/attrs.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* eslint-disable import/prefer-default-export */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return input value as an array.
|
|
5
|
+
*/
|
|
6
|
+
function asArray(input) {
|
|
7
|
+
if (!input) return [];
|
|
8
|
+
return Array.isArray(input) ? input : [input];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Separates class, style, and event listener attributes from other attributes. This allows the
|
|
13
|
+
* classes and styles to be applied to the root element of a component while applying the other
|
|
14
|
+
* attributes and listeners to the inner <input> element of the component. This is useful due to
|
|
15
|
+
* the differences in how Vue 3 and Vue 2 use $attrs. Read more about those differences in
|
|
16
|
+
* https://v3.vuejs.org/guide/migration/attrs-includes-class-style.html and
|
|
17
|
+
* https://v3.vuejs.org/guide/migration/listeners-removed.html.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
*
|
|
21
|
+
* <script>
|
|
22
|
+
* ...
|
|
23
|
+
* emits: ['eventName'],
|
|
24
|
+
* setup(props, context) {
|
|
25
|
+
* const { classes, styles, inputAttrs, inputListeners } = useAttrs(context, ['eventName']);
|
|
26
|
+
* return { classes, styles, inputAttrs, inputListeners };
|
|
27
|
+
* },
|
|
28
|
+
* ...
|
|
29
|
+
* </script>
|
|
30
|
+
*
|
|
31
|
+
* <template>
|
|
32
|
+
* <div :class="classes" :style="styles">
|
|
33
|
+
* ...
|
|
34
|
+
* <input v-bind="inputAttrs" v-on="inputListeners">
|
|
35
|
+
* ...
|
|
36
|
+
* </div>
|
|
37
|
+
* </template>
|
|
38
|
+
*
|
|
39
|
+
* @param context vue component context, from the second argument of the `setup()` function
|
|
40
|
+
* @param ownEvents array of event name strings, same as the `emits` component option
|
|
41
|
+
* @returns { classes, styles, inputAttrs, inputListeners }
|
|
42
|
+
*/
|
|
43
|
+
export function useAttrs({ attrs, listeners }, ownEvents = []) {
|
|
44
|
+
const classes = asArray(attrs?.class);
|
|
45
|
+
const styles = asArray(attrs?.style);
|
|
46
|
+
|
|
47
|
+
const inputListeners = listeners ? { ...listeners } : {};
|
|
48
|
+
ownEvents.forEach((event) => {
|
|
49
|
+
delete inputListeners[event];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const inputAttrs = { ...attrs };
|
|
53
|
+
delete inputAttrs.class;
|
|
54
|
+
delete inputAttrs.style;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
classes,
|
|
58
|
+
styles,
|
|
59
|
+
inputAttrs,
|
|
60
|
+
inputListeners,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
File without changes
|
|
@@ -13,13 +13,21 @@ module.exports = {
|
|
|
13
13
|
'@storybook/addon-storysource',
|
|
14
14
|
'storybook-dark-mode',
|
|
15
15
|
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
name: '@storybook/addon-postcss',
|
|
17
|
+
options: {
|
|
18
|
+
postcssLoaderOptions: {
|
|
19
|
+
implementation: require('postcss'),
|
|
20
|
+
},
|
|
20
21
|
},
|
|
21
22
|
},
|
|
23
|
+
],
|
|
24
|
+
webpackFinal: async (config) => {
|
|
25
|
+
config.module.rules.push({
|
|
26
|
+
test: /\.mjs$/,
|
|
27
|
+
include: /node_modules/,
|
|
28
|
+
type: 'javascript/auto'
|
|
29
|
+
});
|
|
30
|
+
return config;
|
|
22
31
|
},
|
|
23
|
-
],
|
|
24
32
|
"framework": "@storybook/vue"
|
|
25
33
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import './tailwind.css';
|
|
2
2
|
import addons from '@storybook/addons';
|
|
3
3
|
import KvThemeProvider from '../KvThemeProvider.vue';
|
|
4
|
-
import { defaultTheme, darkTheme } from '@kiva/kv-tokens/configs/kivaColors';
|
|
4
|
+
import { defaultTheme, darkTheme } from '@kiva/kv-tokens/configs/kivaColors.cjs';
|
|
5
|
+
import Vue from 'vue';
|
|
6
|
+
import VueCompositionApi from '@vue/composition-api';
|
|
7
|
+
|
|
8
|
+
// Add vue composition api
|
|
9
|
+
Vue.use(VueCompositionApi);
|
|
5
10
|
|
|
6
11
|
export const parameters = {
|
|
7
12
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
package/vue/KvButton.vue
CHANGED
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
</template>
|
|
38
38
|
|
|
39
39
|
<script>
|
|
40
|
+
import {
|
|
41
|
+
computed,
|
|
42
|
+
onMounted,
|
|
43
|
+
ref,
|
|
44
|
+
toRefs,
|
|
45
|
+
watch,
|
|
46
|
+
} from 'vue-demi';
|
|
40
47
|
import KvLoadingSpinner from './KvLoadingSpinner.vue';
|
|
41
48
|
|
|
42
49
|
export default {
|
|
@@ -92,12 +99,20 @@ export default {
|
|
|
92
99
|
},
|
|
93
100
|
},
|
|
94
101
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
emits: [
|
|
103
|
+
'click',
|
|
104
|
+
],
|
|
105
|
+
setup(props, { emit }) {
|
|
106
|
+
const {
|
|
107
|
+
to,
|
|
108
|
+
href,
|
|
109
|
+
type,
|
|
110
|
+
variant,
|
|
111
|
+
state,
|
|
112
|
+
} = toRefs(props);
|
|
113
|
+
|
|
114
|
+
const loadingColor = computed(() => {
|
|
115
|
+
switch (variant.value) {
|
|
101
116
|
case 'secondary':
|
|
102
117
|
return 'black';
|
|
103
118
|
case 'ghost':
|
|
@@ -105,14 +120,15 @@ export default {
|
|
|
105
120
|
default:
|
|
106
121
|
return 'white';
|
|
107
122
|
}
|
|
108
|
-
}
|
|
109
|
-
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const computedClass = computed(() => {
|
|
110
126
|
let classes = '';
|
|
111
|
-
switch (
|
|
127
|
+
switch (variant.value) {
|
|
112
128
|
case 'primary':
|
|
113
129
|
default:
|
|
114
130
|
classes = 'tw-text-primary-inverse';
|
|
115
|
-
if (
|
|
131
|
+
if (state.value === 'active') {
|
|
116
132
|
classes = `${classes} tw-bg-action-highlight tw-border-action-highlight`;
|
|
117
133
|
} else {
|
|
118
134
|
classes = `${classes} tw-bg-action hover:tw-bg-action-highlight tw-border-action hover:tw-border-action-highlight`;
|
|
@@ -120,7 +136,7 @@ export default {
|
|
|
120
136
|
break;
|
|
121
137
|
case 'secondary':
|
|
122
138
|
classes = 'tw-text-primary';
|
|
123
|
-
if (
|
|
139
|
+
if (state.value === 'active') {
|
|
124
140
|
classes = `${classes} tw-bg-secondary tw-border-primary`;
|
|
125
141
|
} else {
|
|
126
142
|
classes = `${classes} tw-bg-primary hover:tw-bg-secondary tw-border-tertiary hover:tw-border-primary`;
|
|
@@ -128,7 +144,7 @@ export default {
|
|
|
128
144
|
break;
|
|
129
145
|
case 'danger':
|
|
130
146
|
classes = 'tw-text-primary-inverse';
|
|
131
|
-
if (
|
|
147
|
+
if (state.value === 'active') {
|
|
132
148
|
classes = `${classes} tw-bg-danger-highlight tw-border-danger-highlight`;
|
|
133
149
|
} else {
|
|
134
150
|
classes = `${classes} tw-bg-danger hover:tw-bg-danger-highlight tw-border-danger hover:tw-border-danger-highlight`;
|
|
@@ -136,7 +152,7 @@ export default {
|
|
|
136
152
|
break;
|
|
137
153
|
case 'link':
|
|
138
154
|
classes = 'tw-bg-primary-inverse tw-text-primary-inverse';
|
|
139
|
-
if (
|
|
155
|
+
if (state.value === 'active') {
|
|
140
156
|
classes = `${classes} tw-border-secondary`;
|
|
141
157
|
} else {
|
|
142
158
|
classes = `${classes} tw-border-primary hover:tw-border-secondary`;
|
|
@@ -144,7 +160,7 @@ export default {
|
|
|
144
160
|
break;
|
|
145
161
|
case 'ghost':
|
|
146
162
|
classes = 'tw-text-primary tw-border-transparent';
|
|
147
|
-
if (
|
|
163
|
+
if (state.value === 'active') {
|
|
148
164
|
classes = `${classes} tw-bg-secondary`;
|
|
149
165
|
} else {
|
|
150
166
|
classes = `${classes} tw-bg-primary hover:tw-bg-secondary`;
|
|
@@ -152,44 +168,31 @@ export default {
|
|
|
152
168
|
break;
|
|
153
169
|
}
|
|
154
170
|
return classes;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
tag() {
|
|
160
|
-
if (
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const isDisabled = computed(() => state.value === 'disabled' || state.value === 'loading');
|
|
174
|
+
|
|
175
|
+
const tag = computed(() => {
|
|
176
|
+
if (to.value) {
|
|
161
177
|
return 'router-link';
|
|
162
178
|
}
|
|
163
|
-
if (
|
|
179
|
+
if (href.value) {
|
|
164
180
|
return 'a';
|
|
165
181
|
}
|
|
166
182
|
return 'button';
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const computedType = computed(() => {
|
|
186
|
+
if (to.value || href.value) {
|
|
170
187
|
return null;
|
|
171
188
|
}
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
watch: { href() { this.setHref(); } },
|
|
176
|
-
mounted() {
|
|
177
|
-
this.setHref();
|
|
178
|
-
},
|
|
179
|
-
methods: {
|
|
180
|
-
onClick(event) {
|
|
181
|
-
// emit a vue event and prevent native event
|
|
182
|
-
// so we don't have to write @click.native in our templates
|
|
183
|
-
if (this.tag === 'button' && this.type !== 'submit') {
|
|
184
|
-
event.preventDefault();
|
|
185
|
-
this.$emit('click', event);
|
|
186
|
-
}
|
|
189
|
+
return type.value;
|
|
190
|
+
});
|
|
187
191
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
createRipple(event) {
|
|
191
|
-
const { buttonRef, buttonInnerRef } = this.$refs;
|
|
192
|
+
const buttonRef = ref(null);
|
|
193
|
+
const buttonInnerRef = ref(null);
|
|
192
194
|
|
|
195
|
+
const createRipple = (event) => {
|
|
193
196
|
// build an element to animate
|
|
194
197
|
const blipEl = document.createElement('span');
|
|
195
198
|
blipEl.classList = `
|
|
@@ -209,7 +212,7 @@ export default {
|
|
|
209
212
|
|
|
210
213
|
// some variants shouldn't have a white blip
|
|
211
214
|
const darkBlipVariants = ['secondary', 'ghost'];
|
|
212
|
-
const blipBgColor = darkBlipVariants.includes(
|
|
215
|
+
const blipBgColor = darkBlipVariants.includes(variant.value) ? 'tw-bg-tertiary' : 'tw-bg-primary';
|
|
213
216
|
blipEl.classList.add(blipBgColor);
|
|
214
217
|
|
|
215
218
|
// position the blip where the pointer click is or center it if keyboard
|
|
@@ -217,7 +220,7 @@ export default {
|
|
|
217
220
|
const { clientX, clientY } = event;
|
|
218
221
|
const {
|
|
219
222
|
offsetLeft, offsetTop, offsetWidth, offsetHeight,
|
|
220
|
-
} = buttonRef;
|
|
223
|
+
} = buttonRef.value;
|
|
221
224
|
let blipX;
|
|
222
225
|
let blipY;
|
|
223
226
|
if (fromClick) {
|
|
@@ -231,20 +234,39 @@ export default {
|
|
|
231
234
|
blipEl.style.setProperty('top', blipY);
|
|
232
235
|
|
|
233
236
|
// append the blip to the button, remove it when the animation is done
|
|
234
|
-
buttonInnerRef.appendChild(blipEl);
|
|
237
|
+
buttonInnerRef.value.appendChild(blipEl);
|
|
235
238
|
blipEl.addEventListener('animationend', function animationComplete() {
|
|
236
|
-
buttonInnerRef.removeChild(blipEl);
|
|
239
|
+
buttonInnerRef.value.removeChild(blipEl);
|
|
237
240
|
blipEl.removeEventListener('animationend', animationComplete);
|
|
238
241
|
});
|
|
239
|
-
}
|
|
240
|
-
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const onClick = (event) => {
|
|
245
|
+
// Pass-through native click event to parent while adding ripple effect
|
|
246
|
+
emit('click', event);
|
|
247
|
+
createRipple(event);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const setHref = () => {
|
|
241
251
|
// if the component is a router-link, router-link will set the href
|
|
242
252
|
// if the href is passed as a prop, use that instead
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
buttonRef.href = this.href;
|
|
253
|
+
if (href.value) {
|
|
254
|
+
buttonRef.value.href = href.value;
|
|
246
255
|
}
|
|
247
|
-
}
|
|
256
|
+
};
|
|
257
|
+
watch(href, () => setHref());
|
|
258
|
+
onMounted(() => setHref());
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
buttonRef,
|
|
262
|
+
buttonInnerRef,
|
|
263
|
+
computedClass,
|
|
264
|
+
computedType,
|
|
265
|
+
isDisabled,
|
|
266
|
+
loadingColor,
|
|
267
|
+
onClick,
|
|
268
|
+
tag,
|
|
269
|
+
};
|
|
248
270
|
},
|
|
249
271
|
};
|
|
250
272
|
</script>
|
package/vue/KvCarousel.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<section
|
|
3
|
-
ref="
|
|
3
|
+
ref="rootEl"
|
|
4
4
|
class="tw-overflow-hidden tw-w-full"
|
|
5
5
|
aria-label="carousel"
|
|
6
6
|
>
|
|
@@ -72,6 +72,15 @@
|
|
|
72
72
|
</template>
|
|
73
73
|
|
|
74
74
|
<script>
|
|
75
|
+
import {
|
|
76
|
+
computed,
|
|
77
|
+
onMounted,
|
|
78
|
+
onUnmounted,
|
|
79
|
+
ref,
|
|
80
|
+
toRefs,
|
|
81
|
+
nextTick,
|
|
82
|
+
getCurrentInstance,
|
|
83
|
+
} from 'vue-demi';
|
|
75
84
|
import EmblaCarousel from 'embla-carousel';
|
|
76
85
|
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
|
77
86
|
import { throttle } from '../utils/throttle';
|
|
@@ -122,84 +131,59 @@ export default {
|
|
|
122
131
|
default: '',
|
|
123
132
|
},
|
|
124
133
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
emits: [
|
|
135
|
+
'change',
|
|
136
|
+
'interact-carousel',
|
|
137
|
+
],
|
|
138
|
+
setup(props, { emit, slots }) {
|
|
139
|
+
const {
|
|
140
|
+
emblaOptions,
|
|
141
|
+
slidesToScroll,
|
|
142
|
+
} = toRefs(props);
|
|
143
|
+
const rootEl = ref(null);
|
|
144
|
+
const embla = ref(null);
|
|
145
|
+
const slides = ref([]);
|
|
146
|
+
const currentIndex = ref(0);
|
|
147
|
+
|
|
148
|
+
const forceUpdate = () => {
|
|
149
|
+
const instance = getCurrentInstance();
|
|
150
|
+
instance.proxy.$forceUpdate();
|
|
132
151
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
152
|
+
|
|
153
|
+
const componentSlotKeys = computed(() => {
|
|
154
|
+
const keys = Object.keys(slots);
|
|
155
|
+
return keys;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const nextIndex = computed(() => {
|
|
159
|
+
const nextSlideIndex = currentIndex.value + 1;
|
|
160
|
+
if (nextSlideIndex < slides.value.length) {
|
|
141
161
|
return nextSlideIndex;
|
|
142
162
|
}
|
|
143
163
|
return 0;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const previousIndex = computed(() => {
|
|
167
|
+
const previousSlideIndex = currentIndex.value - 1;
|
|
147
168
|
if (previousSlideIndex >= 0) {
|
|
148
169
|
return previousSlideIndex;
|
|
149
170
|
}
|
|
150
|
-
return
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
mounted() {
|
|
155
|
-
// initialize Embla
|
|
156
|
-
this.embla = EmblaCarousel(this.$refs.KvCarousel, {
|
|
157
|
-
loop: true,
|
|
158
|
-
containScroll: 'trimSnaps',
|
|
159
|
-
inViewThreshold: 0.9,
|
|
160
|
-
align: 'start',
|
|
161
|
-
...this.emblaOptions,
|
|
171
|
+
return slides.value.length - 1;
|
|
162
172
|
});
|
|
163
173
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.$forceUpdate();
|
|
175
|
-
}, 250),
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// get slide components
|
|
180
|
-
this.slides = this.embla.slideNodes();
|
|
181
|
-
|
|
182
|
-
this.embla.on('select', () => {
|
|
183
|
-
this.currentIndex = this.embla.selectedScrollSnap();
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* The index of the slide that the carousel has changed to
|
|
187
|
-
* @event change
|
|
188
|
-
* @type {Event}
|
|
189
|
-
*/
|
|
190
|
-
this.$emit('change', this.currentIndex);
|
|
191
|
-
});
|
|
192
|
-
},
|
|
193
|
-
beforeDestroy() {
|
|
194
|
-
// clean up event listeners
|
|
195
|
-
this.embla.off('select');
|
|
196
|
-
this.embla.destroy();
|
|
197
|
-
},
|
|
198
|
-
methods: {
|
|
199
|
-
async handleUserInteraction(index, interactionType) {
|
|
174
|
+
/**
|
|
175
|
+
* Jump to a specific slide index
|
|
176
|
+
*
|
|
177
|
+
* @param {Number} num Index of slide to show
|
|
178
|
+
* @public This is a public method
|
|
179
|
+
*/
|
|
180
|
+
const goToSlide = (index) => {
|
|
181
|
+
embla.value.scrollTo(index);
|
|
182
|
+
};
|
|
183
|
+
const handleUserInteraction = async (index, interactionType) => {
|
|
200
184
|
if (index !== null && typeof index !== 'undefined') {
|
|
201
|
-
await
|
|
202
|
-
|
|
185
|
+
await nextTick(); // wait for embla.
|
|
186
|
+
goToSlide(index);
|
|
203
187
|
}
|
|
204
188
|
/**
|
|
205
189
|
* Fires when the user interacts with the carousel.
|
|
@@ -207,48 +191,38 @@ export default {
|
|
|
207
191
|
* @event interact-carousel
|
|
208
192
|
* @type {Event}
|
|
209
193
|
*/
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Jump to a specific slide index
|
|
214
|
-
*
|
|
215
|
-
* @param {Number} num Index of slide to show
|
|
216
|
-
* @public This is a public method
|
|
217
|
-
*/
|
|
218
|
-
goToSlide(index) {
|
|
219
|
-
this.embla.scrollTo(index);
|
|
220
|
-
this.intervalTimerCurrentTime = 0;
|
|
221
|
-
},
|
|
194
|
+
emit('interact-carousel', interactionType);
|
|
195
|
+
};
|
|
222
196
|
/**
|
|
223
197
|
* Reinitialize the carousel.
|
|
224
198
|
* Used after adding slides dynamically.
|
|
225
199
|
*
|
|
226
200
|
* @public This is a public method
|
|
227
201
|
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (this.slidesToScroll === 'visible') {
|
|
231
|
-
this.reInitVisible();
|
|
232
|
-
}
|
|
233
|
-
this.slides = this.embla.slideNodes();
|
|
234
|
-
this.$forceUpdate(); // force a re-render so embla.canScrollNext() gets called in the template
|
|
235
|
-
},
|
|
236
|
-
reInitVisible() {
|
|
237
|
-
const slidesInView = this.embla.slidesInView(true).length;
|
|
202
|
+
const reInitVisible = () => {
|
|
203
|
+
const slidesInView = embla.value.slidesInView(true).length;
|
|
238
204
|
if (slidesInView) {
|
|
239
|
-
|
|
205
|
+
embla.value.reInit({
|
|
240
206
|
slidesToScroll: slidesInView,
|
|
241
207
|
inViewThreshold: 0.9,
|
|
242
208
|
});
|
|
243
209
|
}
|
|
244
|
-
}
|
|
245
|
-
|
|
210
|
+
};
|
|
211
|
+
const reInit = () => {
|
|
212
|
+
embla.value.reInit();
|
|
213
|
+
if (slidesToScroll.value === 'visible') {
|
|
214
|
+
reInitVisible();
|
|
215
|
+
}
|
|
216
|
+
slides.value = embla.value.slideNodes();
|
|
217
|
+
forceUpdate(); // force a re-render so embla.canScrollNext() gets called in the template
|
|
218
|
+
};
|
|
219
|
+
const onCarouselContainerClick = (e) => {
|
|
246
220
|
// If we're dragging, block click handlers within slides
|
|
247
|
-
if (
|
|
221
|
+
if (embla.value && !embla.value.clickAllowed()) {
|
|
248
222
|
e.preventDefault();
|
|
249
223
|
e.stopPropagation();
|
|
250
224
|
}
|
|
251
|
-
}
|
|
225
|
+
};
|
|
252
226
|
/**
|
|
253
227
|
* If the slide is not completely in view in the carousel
|
|
254
228
|
* it should be aria-hidden
|
|
@@ -256,25 +230,87 @@ export default {
|
|
|
256
230
|
* @param {Number} index The current index of the slide (starts at 1)
|
|
257
231
|
* @returns {Boolean}
|
|
258
232
|
*/
|
|
259
|
-
isAriaHidden(index) {
|
|
233
|
+
const isAriaHidden = (index) => {
|
|
260
234
|
// Index starts at 1
|
|
261
235
|
// Embla starts at 0
|
|
262
|
-
if (
|
|
263
|
-
return !
|
|
236
|
+
if (embla.value) {
|
|
237
|
+
return !embla.value.slidesInView(true).includes(index - 1);
|
|
264
238
|
}
|
|
265
239
|
return false;
|
|
266
|
-
}
|
|
240
|
+
};
|
|
267
241
|
/**
|
|
268
242
|
* Returns number of slides in the carousel
|
|
269
243
|
*
|
|
270
244
|
* @returns {Number}
|
|
271
245
|
*/
|
|
272
|
-
slideIndicatorListLength() {
|
|
273
|
-
|
|
274
|
-
|
|
246
|
+
const slideIndicatorListLength = () => {
|
|
247
|
+
const indicator = embla.value ? embla.value.scrollSnapList().length : 0;
|
|
248
|
+
return indicator;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
onMounted(async () => {
|
|
252
|
+
getCurrentInstance();
|
|
253
|
+
embla.value = EmblaCarousel(rootEl.value, {
|
|
254
|
+
loop: true,
|
|
255
|
+
containScroll: 'trimSnaps',
|
|
256
|
+
inViewThreshold: 0.9,
|
|
257
|
+
align: 'start',
|
|
258
|
+
...emblaOptions.value,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (slidesToScroll.value === 'visible') {
|
|
262
|
+
reInitVisible();
|
|
263
|
+
|
|
264
|
+
embla.value.on(
|
|
265
|
+
'resize',
|
|
266
|
+
throttle(() => {
|
|
267
|
+
embla.value.reInit({
|
|
268
|
+
slidesToScroll: embla.value.slidesInView(true).length,
|
|
269
|
+
inViewThreshold: 0.9,
|
|
270
|
+
});
|
|
271
|
+
forceUpdate();
|
|
272
|
+
}, 250),
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// get slide components
|
|
277
|
+
slides.value = embla.value.slideNodes();
|
|
278
|
+
|
|
279
|
+
embla.value.on('select', () => {
|
|
280
|
+
currentIndex.value = embla.value.selectedScrollSnap();
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The index of the slide that the carousel has changed to
|
|
284
|
+
* @event change
|
|
285
|
+
* @type {Event}
|
|
286
|
+
*/
|
|
287
|
+
emit('change', currentIndex);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
onUnmounted(async () => {
|
|
292
|
+
embla.value.off('select');
|
|
293
|
+
embla.value.destroy();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
rootEl,
|
|
298
|
+
mdiChevronLeft,
|
|
299
|
+
mdiChevronRight,
|
|
300
|
+
embla,
|
|
301
|
+
slides,
|
|
302
|
+
currentIndex,
|
|
303
|
+
componentSlotKeys,
|
|
304
|
+
nextIndex,
|
|
305
|
+
previousIndex,
|
|
306
|
+
handleUserInteraction,
|
|
307
|
+
goToSlide,
|
|
308
|
+
reInit,
|
|
309
|
+
reInitVisible,
|
|
310
|
+
onCarouselContainerClick,
|
|
311
|
+
isAriaHidden,
|
|
312
|
+
slideIndicatorListLength,
|
|
313
|
+
};
|
|
275
314
|
},
|
|
276
315
|
};
|
|
277
316
|
</script>
|
|
278
|
-
|
|
279
|
-
<style scoped>
|
|
280
|
-
</style>
|