@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.
Files changed (50) hide show
  1. package/{.eslintrc.js → .eslintrc.cjs} +1 -1
  2. package/CHANGELOG.md +69 -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 +38 -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 +32 -0
  17. package/tests/unit/specs/components/KvTabs.spec.js +167 -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 +75 -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 +73 -55
  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
@@ -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
- name: '@storybook/addon-postcss',
17
- options: {
18
- postcssLoaderOptions: {
19
- implementation: require('postcss'),
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
- data() {
96
- return {};
97
- },
98
- computed: {
99
- loadingColor() {
100
- switch (this.variant) {
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
- computedClass() {
123
+ });
124
+
125
+ const computedClass = computed(() => {
110
126
  let classes = '';
111
- switch (this.variant) {
127
+ switch (variant.value) {
112
128
  case 'primary':
113
129
  default:
114
130
  classes = 'tw-text-primary-inverse';
115
- if (this.state === 'active') {
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 (this.state === 'active') {
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 (this.state === 'active') {
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 (this.state === 'active') {
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 (this.state === 'active') {
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
- isDisabled() {
157
- return this.state === 'disabled' || this.state === 'loading';
158
- },
159
- tag() {
160
- if (this.to) {
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 (this.href) {
179
+ if (href.value) {
164
180
  return 'a';
165
181
  }
166
182
  return 'button';
167
- },
168
- computedType() {
169
- if (this.to || this.href) {
183
+ });
184
+
185
+ const computedType = computed(() => {
186
+ if (to.value || href.value) {
170
187
  return null;
171
188
  }
172
- return this.type;
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
- this.createRipple(event);
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(this.variant) ? 'tw-bg-tertiary' : 'tw-bg-primary';
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
- setHref() {
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 (this.href) {
244
- const { buttonRef } = this.$refs;
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>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <section
3
- ref="KvCarousel"
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
- data() {
126
- return {
127
- mdiChevronLeft,
128
- mdiChevronRight,
129
- embla: null,
130
- slides: [],
131
- currentIndex: 0,
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
- computed: {
135
- componentSlotKeys() {
136
- return Object.keys(this.$slots);
137
- },
138
- nextIndex() {
139
- const nextSlideIndex = this.currentIndex + 1;
140
- if (nextSlideIndex < this.slides.length) {
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
- previousIndex() {
146
- const previousSlideIndex = this.currentIndex - 1;
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 this.slides.length - 1;
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
- if (this.slidesToScroll === 'visible') {
165
- this.reInitVisible();
166
-
167
- this.embla.on(
168
- 'resize',
169
- throttle(() => {
170
- this.embla.reInit({
171
- slidesToScroll: this.embla.slidesInView(true).length,
172
- inViewThreshold: 0.9,
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 this.$nextTick(); // wait for embla.
202
- this.goToSlide(index);
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
- this.$emit('interact-carousel', interactionType);
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
- reInit() {
229
- this.embla.reInit();
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
- this.embla.reInit({
205
+ embla.value.reInit({
240
206
  slidesToScroll: slidesInView,
241
207
  inViewThreshold: 0.9,
242
208
  });
243
209
  }
244
- },
245
- onCarouselContainerClick(e) {
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 (this.embla && !this.embla.clickAllowed()) {
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 (this.embla) {
263
- return !this.embla.slidesInView(true).includes(index - 1);
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
- return this.embla ? this.embla.scrollSnapList().length : 0;
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>