Skip to content
Permalink
Browse files

feat(b-carousel): add prop `no-wrap` for disabling wrapping to start/…

…end (closes #3902) (#3905)

* feat(b-carousel): add prop `no-wrap` for disabling wrapping to start/end (closes #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
  • Loading branch information...
tmorehouse authored and jackmu95 committed Aug 17, 2019
1 parent eddccc4 commit 2c8bd23f6702b455f0686da6e97b9edae182ed5c
@@ -209,6 +209,13 @@ Set the `<b-carousel>` `no-animation` prop to `true` to disable slide animation.
<!-- b-carousel-no-animation.vue -->
```

## Slide wrapping

Normally when the carousel reaches one end or the other in the list of slides, it will wrap to the
opposite end of the list of slides and continue cycling.

To disable carousel slide wrapping, set the `no-wrap` prop to true.

## Hide slide text content on small screens

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`.
Programmatically control which slide is showing via `v-model` (which binds to the `value` prop).
Note, that slides are indexed starting at `0`.

## Programmatic slide control

The `<b-carousel>` instance provides several public methods for controlling sliding:

| Method | Description |
| ----------------- | ------------------------------------------------------- |
| `setSlide(index)` | Go to slide specified by `index` |
| `next()` | Go to next slide |
| `prev()` | Go to previous slide |
| `pause()` | Pause the slide cycling |
| `start()` | Start slide cycling (prop `interval` must have a value) |

You will need a reference (via `this.$refs`) to the carousel instance in order to call these
methods:

```html
<template>
<b-carousel ref="myCarousel" .... >
<!-- slides go here -->
</b-carousel>
</template>

<script>
export default {
// ...
methods: {
prev() {
this.$refs.myCarousel.prev()
},
next() {
this.$refs.myCarousel.next()
}
}
}
</script>
```

## Accessibility

Carousels are generally not fully compliant with accessibility standards, although we try to make
@@ -70,7 +70,7 @@ const getTransitionEndEvent = el => {

// @vue/component
export const BCarousel = /*#__PURE__*/ Vue.extend({
name: 'BCarousel',
name: NAME,
mixins: [idMixin, normalizeSlotMixin],
provide() {
return { bvCarousel: this }
@@ -118,6 +118,11 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
type: Boolean,
default: false
},
noWrap: {
// Disable wrapping/looping when start/end is reached
type: Boolean,
default: false
},
noTouch: {
// Sniffed by carousel-slide
type: Boolean,
@@ -160,10 +165,15 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
touchDeltaX: 0
}
},
computed: {
numSlides() {
return this.slides.length
}
},
watch: {
value(newVal, oldVal) {
if (newVal !== oldVal) {
this.setSlide(newVal)
this.setSlide(parseInt(newVal, 10) || 0)
}
},
interval(newVal, oldVal) {
@@ -230,9 +240,12 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
if (isBrowser && document.visibilityState && document.hidden) {
return
}
const len = this.slides.length
const noWrap = this.noWrap
const numSlides = this.numSlides
// Make sure we have an integer (you never know!)
slide = Math.floor(slide)
// Don't do anything if nothing to slide to
if (len === 0) {
if (numSlides === 0) {
return
}
// Don't change slide while transitioning, wait until transition is done
@@ -242,10 +255,23 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
return
}
this.direction = direction
// Make sure we have an integer (you never know!)
slide = Math.floor(slide)
// Set new slide index. Wrap around if necessary
this.index = slide >= len ? 0 : slide >= 0 ? slide : len - 1
// Set new slide index
// Wrap around if necessary (if no-wrap not enabled)
this.index =
slide >= numSlides
? noWrap
? numSlides - 1
: 0
: slide < 0
? noWrap
? 0
: numSlides - 1
: slide
// Ensure the v-model is synched up if no-wrap is enabled
// and user tried to slide pass either ends
if (noWrap && this.index !== slide && this.index !== this.value) {
this.$emit('input', this.index)
}
},
// Previous slide
prev() {
@@ -276,7 +302,7 @@ export const BCarousel = /*#__PURE__*/ Vue.extend({
this._intervalId = null
}
// Don't start if no interval, or less than 2 slides
if (this.interval && this.slides.length > 1) {
if (this.interval && this.numSlides > 1) {
this._intervalId = setInterval(this.next, Math.max(1000, this.interval))
}
},
@@ -14,6 +14,7 @@ const appDef = {
controls: false,
fade: false,
noAnimation: false,
noWrap: false,
value: 0
},
render(h) {
@@ -26,6 +27,7 @@ const appDef = {
controls: this.controls,
fade: this.fade,
noAnimation: this.noAnimation,
noWrap: this.noWrap,
value: this.value
}
},
@@ -995,4 +997,178 @@ describe('carousel', () => {

wrapper.destroy()
})

it('Next/Prev slide wraps to end/start when no-wrap is false', async () => {
const wrapper = mount(localVue.extend(appDef), {
localVue: localVue,
attachToDocument: true,
propsData: {
interval: 0,
fade: false,
noAnimation: true,
noWrap: false,
indicators: true,
controls: true,
// Start at last slide
value: 3
}
})

expect(wrapper.isVueInstance()).toBe(true)
const $carousel = wrapper.find(BCarousel)
expect($carousel).toBeDefined()
expect($carousel.isVueInstance()).toBe(true)

await waitNT(wrapper.vm)
await waitRAF()

const $indicators = $carousel.findAll('.carousel-indicators > li')
expect($indicators.length).toBe(4)

expect($carousel.emitted('sliding-start')).not.toBeDefined()
expect($carousel.emitted('sliding-end')).not.toBeDefined()
expect($carousel.emitted('input')).not.toBeDefined()

expect($carousel.vm.index).toBe(3)
expect($carousel.vm.isSliding).toBe(false)

// Transitions (or fallback timers) are not used when no-animation set
// Call vm.next()
$carousel.vm.next()
await waitNT(wrapper.vm)

expect($carousel.emitted('sliding-start')).toBeDefined()
expect($carousel.emitted('sliding-end')).toBeDefined()
expect($carousel.emitted('sliding-start').length).toBe(1)
expect($carousel.emitted('sliding-end').length).toBe(1)
// Should have index of 0
expect($carousel.emitted('sliding-start')[0][0]).toEqual(0)
expect($carousel.emitted('sliding-end')[0][0]).toEqual(0)
expect($carousel.emitted('input')).toBeDefined()
expect($carousel.emitted('input').length).toBe(1)
expect($carousel.emitted('input')[0][0]).toEqual(0)
expect($carousel.vm.index).toBe(0)
expect($carousel.vm.isSliding).toBe(false)

// Call vm.prev()
$carousel.vm.prev()
await waitNT(wrapper.vm)

expect($carousel.emitted('sliding-start').length).toBe(2)
expect($carousel.emitted('sliding-end').length).toBe(2)
// Should have index set to last slide
expect($carousel.emitted('sliding-start')[1][0]).toEqual(3)
expect($carousel.emitted('sliding-end')[1][0]).toEqual(3)
expect($carousel.emitted('input').length).toBe(2)
expect($carousel.emitted('input')[1][0]).toEqual(3)
expect($carousel.vm.index).toBe(3)
expect($carousel.vm.isSliding).toBe(false)

wrapper.destroy()
})

it('Next/Prev slide does not wrap to end/start when no-wrap is true', async () => {
const wrapper = mount(localVue.extend(appDef), {
localVue: localVue,
attachToDocument: true,
propsData: {
interval: 0,
fade: false,
// Transitions (or fallback timers) are not used when no-animation set
noAnimation: true,
noWrap: true,
indicators: true,
controls: true,
// Start at last slide
value: 3
}
})

expect(wrapper.isVueInstance()).toBe(true)
const $carousel = wrapper.find(BCarousel)
expect($carousel).toBeDefined()
expect($carousel.isVueInstance()).toBe(true)

await waitNT(wrapper.vm)
await waitRAF()

const $indicators = $carousel.findAll('.carousel-indicators > li')
expect($indicators.length).toBe(4)

expect($carousel.emitted('sliding-start')).not.toBeDefined()
expect($carousel.emitted('sliding-end')).not.toBeDefined()
expect($carousel.emitted('input')).not.toBeDefined()

expect($carousel.vm.index).toBe(3)
expect($carousel.vm.isSliding).toBe(false)

// Call vm.next()
$carousel.vm.next()
await waitNT(wrapper.vm)

// Should not slide to start
expect($carousel.emitted('sliding-start')).not.toBeDefined()
expect($carousel.emitted('sliding-end')).not.toBeDefined()
// Should have index of 3 (no input event emitted since value set to 3)
expect($carousel.emitted('input')).not.toBeDefined()
expect($carousel.vm.index).toBe(3)
expect($carousel.vm.isSliding).toBe(false)

// Call vm.prev()
$carousel.vm.prev()
await waitNT(wrapper.vm)

expect($carousel.emitted('sliding-start').length).toBe(1)
expect($carousel.emitted('sliding-end').length).toBe(1)
// Should have index set to 2
expect($carousel.emitted('sliding-start')[0][0]).toEqual(2)
expect($carousel.emitted('sliding-end')[0][0]).toEqual(2)
expect($carousel.emitted('input')).toBeDefined()
expect($carousel.emitted('input').length).toBe(1)
expect($carousel.emitted('input')[0][0]).toEqual(2)
expect($carousel.vm.index).toBe(2)
expect($carousel.vm.isSliding).toBe(false)

// Call vm.prev()
$carousel.vm.prev()
await waitNT(wrapper.vm)

expect($carousel.emitted('sliding-start').length).toBe(2)
expect($carousel.emitted('sliding-end').length).toBe(2)
// Should have index set to 1
expect($carousel.emitted('sliding-start')[1][0]).toEqual(1)
expect($carousel.emitted('sliding-end')[1][0]).toEqual(1)
expect($carousel.emitted('input').length).toBe(2)
expect($carousel.emitted('input')[1][0]).toEqual(1)
expect($carousel.vm.index).toBe(1)
expect($carousel.vm.isSliding).toBe(false)

// Call vm.prev()
$carousel.vm.prev()
await waitNT(wrapper.vm)

expect($carousel.emitted('sliding-start').length).toBe(3)
expect($carousel.emitted('sliding-end').length).toBe(3)
// Should have index set to 0
expect($carousel.emitted('sliding-start')[2][0]).toEqual(0)
expect($carousel.emitted('sliding-end')[2][0]).toEqual(0)
expect($carousel.emitted('input').length).toBe(3)
expect($carousel.emitted('input')[2][0]).toEqual(0)
expect($carousel.vm.index).toBe(0)
expect($carousel.vm.isSliding).toBe(false)

// Call vm.prev() (should not wrap)
$carousel.vm.prev()
await waitNT(wrapper.vm)

expect($carousel.emitted('sliding-start').length).toBe(3)
expect($carousel.emitted('sliding-end').length).toBe(3)
// Should have index still set to 0, and emit input to update v-model
expect($carousel.emitted('input').length).toBe(4)
expect($carousel.emitted('input')[3][0]).toEqual(0)
expect($carousel.vm.index).toBe(0)
expect($carousel.vm.isSliding).toBe(false)

wrapper.destroy()
})
})
@@ -14,7 +14,6 @@ export declare class BCarousel extends BvComponent {
next: () => void
start: () => void
pause: () => void
restart: () => void
}

// Component: b-carousel-slide

0 comments on commit 2c8bd23

Please sign in to comment.
You can’t perform that action at this time.