Skip to content

Commit 2c8bd23

Browse files
tmorehousejacobmllr95
authored andcommitted
feat(b-carousel): add prop no-wrap for disabling wrapping to start/end (closes bootstrap-vue#3902) (bootstrap-vue#3905)
* feat(b-carousel): add prop `no-wrap` for disabling wrapping to start/end (closes bootstrap-vue#3902) * Update carousel.js * Update carousel.js * Update carousel.spec.js * Update carousel.spec.js * Update carousel.spec.js * Update README.md * Update README.md * Update index.d.ts * Update README.md * Update carousel.js
1 parent eddccc4 commit 2c8bd23

File tree

4 files changed

+255
-10
lines changed

4 files changed

+255
-10
lines changed

src/components/carousel/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ Set the `<b-carousel>` `no-animation` prop to `true` to disable slide animation.
209209
<!-- b-carousel-no-animation.vue -->
210210
```
211211

212+
## Slide wrapping
213+
214+
Normally when the carousel reaches one end or the other in the list of slides, it will wrap to the
215+
opposite end of the list of slides and continue cycling.
216+
217+
To disable carousel slide wrapping, set the `no-wrap` prop to true.
218+
212219
## Hide slide text content on small screens
213220

214221
On smaller screens you may want to hide the captions and headings. You can do so via the
@@ -226,6 +233,43 @@ disable touch control, set the `no-touch` prop to `true`.
226233
Programmatically control which slide is showing via `v-model` (which binds to the `value` prop).
227234
Note, that slides are indexed starting at `0`.
228235

236+
## Programmatic slide control
237+
238+
The `<b-carousel>` instance provides several public methods for controlling sliding:
239+
240+
| Method | Description |
241+
| ----------------- | ------------------------------------------------------- |
242+
| `setSlide(index)` | Go to slide specified by `index` |
243+
| `next()` | Go to next slide |
244+
| `prev()` | Go to previous slide |
245+
| `pause()` | Pause the slide cycling |
246+
| `start()` | Start slide cycling (prop `interval` must have a value) |
247+
248+
You will need a reference (via `this.$refs`) to the carousel instance in order to call these
249+
methods:
250+
251+
```html
252+
<template>
253+
<b-carousel ref="myCarousel" .... >
254+
<!-- slides go here -->
255+
</b-carousel>
256+
</template>
257+
258+
<script>
259+
export default {
260+
// ...
261+
methods: {
262+
prev() {
263+
this.$refs.myCarousel.prev()
264+
},
265+
next() {
266+
this.$refs.myCarousel.next()
267+
}
268+
}
269+
}
270+
</script>
271+
```
272+
229273
## Accessibility
230274

231275
Carousels are generally not fully compliant with accessibility standards, although we try to make

src/components/carousel/carousel.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ const getTransitionEndEvent = el => {
7070

7171
// @vue/component
7272
export const BCarousel = /*#__PURE__*/ Vue.extend({
73-
name: 'BCarousel',
73+
name: NAME,
7474
mixins: [idMixin, normalizeSlotMixin],
7575
provide() {
7676
return { bvCarousel: this }
@@ -118,6 +118,11 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
118118
type: Boolean,
119119
default: false
120120
},
121+
noWrap: {
122+
// Disable wrapping/looping when start/end is reached
123+
type: Boolean,
124+
default: false
125+
},
121126
noTouch: {
122127
// Sniffed by carousel-slide
123128
type: Boolean,
@@ -160,10 +165,15 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
160165
touchDeltaX: 0
161166
}
162167
},
168+
computed: {
169+
numSlides() {
170+
return this.slides.length
171+
}
172+
},
163173
watch: {
164174
value(newVal, oldVal) {
165175
if (newVal !== oldVal) {
166-
this.setSlide(newVal)
176+
this.setSlide(parseInt(newVal, 10) || 0)
167177
}
168178
},
169179
interval(newVal, oldVal) {
@@ -230,9 +240,12 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
230240
if (isBrowser && document.visibilityState && document.hidden) {
231241
return
232242
}
233-
const len = this.slides.length
243+
const noWrap = this.noWrap
244+
const numSlides = this.numSlides
245+
// Make sure we have an integer (you never know!)
246+
slide = Math.floor(slide)
234247
// Don't do anything if nothing to slide to
235-
if (len === 0) {
248+
if (numSlides === 0) {
236249
return
237250
}
238251
// Don't change slide while transitioning, wait until transition is done
@@ -242,10 +255,23 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
242255
return
243256
}
244257
this.direction = direction
245-
// Make sure we have an integer (you never know!)
246-
slide = Math.floor(slide)
247-
// Set new slide index. Wrap around if necessary
248-
this.index = slide >= len ? 0 : slide >= 0 ? slide : len - 1
258+
// Set new slide index
259+
// Wrap around if necessary (if no-wrap not enabled)
260+
this.index =
261+
slide >= numSlides
262+
? noWrap
263+
? numSlides - 1
264+
: 0
265+
: slide < 0
266+
? noWrap
267+
? 0
268+
: numSlides - 1
269+
: slide
270+
// Ensure the v-model is synched up if no-wrap is enabled
271+
// and user tried to slide pass either ends
272+
if (noWrap && this.index !== slide && this.index !== this.value) {
273+
this.$emit('input', this.index)
274+
}
249275
},
250276
// Previous slide
251277
prev() {
@@ -276,7 +302,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
276302
this._intervalId = null
277303
}
278304
// Don't start if no interval, or less than 2 slides
279-
if (this.interval && this.slides.length > 1) {
305+
if (this.interval && this.numSlides > 1) {
280306
this._intervalId = setInterval(this.next, Math.max(1000, this.interval))
281307
}
282308
},

src/components/carousel/carousel.spec.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const appDef = {
1414
controls: false,
1515
fade: false,
1616
noAnimation: false,
17+
noWrap: false,
1718
value: 0
1819
},
1920
render(h) {
@@ -26,6 +27,7 @@ const appDef = {
2627
controls: this.controls,
2728
fade: this.fade,
2829
noAnimation: this.noAnimation,
30+
noWrap: this.noWrap,
2931
value: this.value
3032
}
3133
},
@@ -995,4 +997,178 @@ describe('carousel', () => {
995997

996998
wrapper.destroy()
997999
})
1000+
1001+
it('Next/Prev slide wraps to end/start when no-wrap is false', async () => {
1002+
const wrapper = mount(localVue.extend(appDef), {
1003+
localVue: localVue,
1004+
attachToDocument: true,
1005+
propsData: {
1006+
interval: 0,
1007+
fade: false,
1008+
noAnimation: true,
1009+
noWrap: false,
1010+
indicators: true,
1011+
controls: true,
1012+
// Start at last slide
1013+
value: 3
1014+
}
1015+
})
1016+
1017+
expect(wrapper.isVueInstance()).toBe(true)
1018+
const $carousel = wrapper.find(BCarousel)
1019+
expect($carousel).toBeDefined()
1020+
expect($carousel.isVueInstance()).toBe(true)
1021+
1022+
await waitNT(wrapper.vm)
1023+
await waitRAF()
1024+
1025+
const $indicators = $carousel.findAll('.carousel-indicators > li')
1026+
expect($indicators.length).toBe(4)
1027+
1028+
expect($carousel.emitted('sliding-start')).not.toBeDefined()
1029+
expect($carousel.emitted('sliding-end')).not.toBeDefined()
1030+
expect($carousel.emitted('input')).not.toBeDefined()
1031+
1032+
expect($carousel.vm.index).toBe(3)
1033+
expect($carousel.vm.isSliding).toBe(false)
1034+
1035+
// Transitions (or fallback timers) are not used when no-animation set
1036+
// Call vm.next()
1037+
$carousel.vm.next()
1038+
await waitNT(wrapper.vm)
1039+
1040+
expect($carousel.emitted('sliding-start')).toBeDefined()
1041+
expect($carousel.emitted('sliding-end')).toBeDefined()
1042+
expect($carousel.emitted('sliding-start').length).toBe(1)
1043+
expect($carousel.emitted('sliding-end').length).toBe(1)
1044+
// Should have index of 0
1045+
expect($carousel.emitted('sliding-start')[0][0]).toEqual(0)
1046+
expect($carousel.emitted('sliding-end')[0][0]).toEqual(0)
1047+
expect($carousel.emitted('input')).toBeDefined()
1048+
expect($carousel.emitted('input').length).toBe(1)
1049+
expect($carousel.emitted('input')[0][0]).toEqual(0)
1050+
expect($carousel.vm.index).toBe(0)
1051+
expect($carousel.vm.isSliding).toBe(false)
1052+
1053+
// Call vm.prev()
1054+
$carousel.vm.prev()
1055+
await waitNT(wrapper.vm)
1056+
1057+
expect($carousel.emitted('sliding-start').length).toBe(2)
1058+
expect($carousel.emitted('sliding-end').length).toBe(2)
1059+
// Should have index set to last slide
1060+
expect($carousel.emitted('sliding-start')[1][0]).toEqual(3)
1061+
expect($carousel.emitted('sliding-end')[1][0]).toEqual(3)
1062+
expect($carousel.emitted('input').length).toBe(2)
1063+
expect($carousel.emitted('input')[1][0]).toEqual(3)
1064+
expect($carousel.vm.index).toBe(3)
1065+
expect($carousel.vm.isSliding).toBe(false)
1066+
1067+
wrapper.destroy()
1068+
})
1069+
1070+
it('Next/Prev slide does not wrap to end/start when no-wrap is true', async () => {
1071+
const wrapper = mount(localVue.extend(appDef), {
1072+
localVue: localVue,
1073+
attachToDocument: true,
1074+
propsData: {
1075+
interval: 0,
1076+
fade: false,
1077+
// Transitions (or fallback timers) are not used when no-animation set
1078+
noAnimation: true,
1079+
noWrap: true,
1080+
indicators: true,
1081+
controls: true,
1082+
// Start at last slide
1083+
value: 3
1084+
}
1085+
})
1086+
1087+
expect(wrapper.isVueInstance()).toBe(true)
1088+
const $carousel = wrapper.find(BCarousel)
1089+
expect($carousel).toBeDefined()
1090+
expect($carousel.isVueInstance()).toBe(true)
1091+
1092+
await waitNT(wrapper.vm)
1093+
await waitRAF()
1094+
1095+
const $indicators = $carousel.findAll('.carousel-indicators > li')
1096+
expect($indicators.length).toBe(4)
1097+
1098+
expect($carousel.emitted('sliding-start')).not.toBeDefined()
1099+
expect($carousel.emitted('sliding-end')).not.toBeDefined()
1100+
expect($carousel.emitted('input')).not.toBeDefined()
1101+
1102+
expect($carousel.vm.index).toBe(3)
1103+
expect($carousel.vm.isSliding).toBe(false)
1104+
1105+
// Call vm.next()
1106+
$carousel.vm.next()
1107+
await waitNT(wrapper.vm)
1108+
1109+
// Should not slide to start
1110+
expect($carousel.emitted('sliding-start')).not.toBeDefined()
1111+
expect($carousel.emitted('sliding-end')).not.toBeDefined()
1112+
// Should have index of 3 (no input event emitted since value set to 3)
1113+
expect($carousel.emitted('input')).not.toBeDefined()
1114+
expect($carousel.vm.index).toBe(3)
1115+
expect($carousel.vm.isSliding).toBe(false)
1116+
1117+
// Call vm.prev()
1118+
$carousel.vm.prev()
1119+
await waitNT(wrapper.vm)
1120+
1121+
expect($carousel.emitted('sliding-start').length).toBe(1)
1122+
expect($carousel.emitted('sliding-end').length).toBe(1)
1123+
// Should have index set to 2
1124+
expect($carousel.emitted('sliding-start')[0][0]).toEqual(2)
1125+
expect($carousel.emitted('sliding-end')[0][0]).toEqual(2)
1126+
expect($carousel.emitted('input')).toBeDefined()
1127+
expect($carousel.emitted('input').length).toBe(1)
1128+
expect($carousel.emitted('input')[0][0]).toEqual(2)
1129+
expect($carousel.vm.index).toBe(2)
1130+
expect($carousel.vm.isSliding).toBe(false)
1131+
1132+
// Call vm.prev()
1133+
$carousel.vm.prev()
1134+
await waitNT(wrapper.vm)
1135+
1136+
expect($carousel.emitted('sliding-start').length).toBe(2)
1137+
expect($carousel.emitted('sliding-end').length).toBe(2)
1138+
// Should have index set to 1
1139+
expect($carousel.emitted('sliding-start')[1][0]).toEqual(1)
1140+
expect($carousel.emitted('sliding-end')[1][0]).toEqual(1)
1141+
expect($carousel.emitted('input').length).toBe(2)
1142+
expect($carousel.emitted('input')[1][0]).toEqual(1)
1143+
expect($carousel.vm.index).toBe(1)
1144+
expect($carousel.vm.isSliding).toBe(false)
1145+
1146+
// Call vm.prev()
1147+
$carousel.vm.prev()
1148+
await waitNT(wrapper.vm)
1149+
1150+
expect($carousel.emitted('sliding-start').length).toBe(3)
1151+
expect($carousel.emitted('sliding-end').length).toBe(3)
1152+
// Should have index set to 0
1153+
expect($carousel.emitted('sliding-start')[2][0]).toEqual(0)
1154+
expect($carousel.emitted('sliding-end')[2][0]).toEqual(0)
1155+
expect($carousel.emitted('input').length).toBe(3)
1156+
expect($carousel.emitted('input')[2][0]).toEqual(0)
1157+
expect($carousel.vm.index).toBe(0)
1158+
expect($carousel.vm.isSliding).toBe(false)
1159+
1160+
// Call vm.prev() (should not wrap)
1161+
$carousel.vm.prev()
1162+
await waitNT(wrapper.vm)
1163+
1164+
expect($carousel.emitted('sliding-start').length).toBe(3)
1165+
expect($carousel.emitted('sliding-end').length).toBe(3)
1166+
// Should have index still set to 0, and emit input to update v-model
1167+
expect($carousel.emitted('input').length).toBe(4)
1168+
expect($carousel.emitted('input')[3][0]).toEqual(0)
1169+
expect($carousel.vm.index).toBe(0)
1170+
expect($carousel.vm.isSliding).toBe(false)
1171+
1172+
wrapper.destroy()
1173+
})
9981174
})

src/components/carousel/index.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export declare class BCarousel extends BvComponent {
1414
next: () => void
1515
start: () => void
1616
pause: () => void
17-
restart: () => void
1817
}
1918

2019
// Component: b-carousel-slide

0 commit comments

Comments
 (0)