Skip to content

Commit

Permalink
feat(b-pagination/b-pagination-nav): allow page change to be prevented (
Browse files Browse the repository at this point in the history
closes #5679) (#5755)

* feat(b-pagination/b-pagination-nav): allow page change to be prevented

* Update pagination-nav.spec.js

* Update pagination.spec.js
  • Loading branch information
jacobmllr95 committed Sep 10, 2020
1 parent d83a2b1 commit 7e18c61
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 104 deletions.
13 changes: 13 additions & 0 deletions src/components/pagination-nav/README.md
Expand Up @@ -490,6 +490,19 @@ To disable auto active page detection, set the `no-page-detect` prop to `true`.
detected. For larger `number-of-pages`, this check can take some time so you may want to manually
control which page is the active via the `v-model` and the `no-page-detect` prop.

## Preventing a page from being selected

You can listen for the `page-click` event, which provides an option to prevent the page from being
selected. The event is emitted with two arguments:

- `bvEvent`: The `BvEvent` object. Call `bvEvt.preventDefault()` to cancel page selection
- `page`: Page number to select (starting with `1`)

For accessibility reasons, when using the `page-click` event to prevent a page from being selected,
you should provide some means of notification to the user as to why the page is not able to be
selected. It is recommended to use the `disabled` attribute on the `<b-pagination-nav>` component
instead of using the `page-click` event (as `disabled` is more intuitive for screen reader users).

## Accessibility

The `<b-pagination-nav>` component provides many features to support assistive technology users,
Expand Down
24 changes: 20 additions & 4 deletions src/components/pagination-nav/package.json
Expand Up @@ -154,21 +154,37 @@
"events": [
{
"event": "input",
"description": "when page changes via user interaction or programmatically",
"description": "Emitted when page changes via user interaction or programmatically",
"args": [
{
"arg": "page",
"description": "Selected page number (starting with 1), or null if no page found"
"description": "Selected page number (starting with `1`), or `null` if no page found"
}
]
},
{
"event": "change",
"description": "when page changes via user interaction",
"description": "Emitted when page changes via user interaction",
"args": [
{
"arg": "page",
"description": "Selected page number (starting with 1)"
"description": "Selected page number (starting with `1`)"
}
]
},
{
"event": "page-click",
"description": "Emitted when a page button was clicked. Cancelable",
"version": "2.17.0",
"args": [
{
"arg": "bvEvt",
"type": "BvEvent",
"description": "The `BvEvent` object. Call `bvEvt.preventDefault()` to cancel page selection"
},
{
"arg": "page",
"description": "Page number to select (starting with `1`)"
}
]
}
Expand Down
37 changes: 26 additions & 11 deletions src/components/pagination-nav/pagination-nav.js
@@ -1,5 +1,6 @@
import Vue from '../../utils/vue'
import looseEqual from '../../utils/loose-equal'
import { BvEvent } from '../../utils/bv-event.class'
import { getComponentConfig } from '../../utils/config'
import { attemptBlur, requestAF } from '../../utils/dom'
import { isBrowser } from '../../utils/env'
Expand Down Expand Up @@ -129,23 +130,37 @@ export const BPaginationNav = /*#__PURE__*/ Vue.extend({
this.guessCurrentPage()
})
},
onClick(pageNum, evt) {
onClick(evt, pageNumber) {
// Dont do anything if clicking the current active page
if (pageNum === this.currentPage) {
if (pageNumber === this.currentPage) {
return
}

const target = evt.currentTarget || evt.target

// Emit a user-cancelable `page-click` event
const clickEvt = new BvEvent('page-click', {
cancelable: true,
vueTarget: this,
target
})
this.$emit(clickEvt.type, clickEvt, pageNumber)
if (clickEvt.defaultPrevented) {
return
}

// Update the `v-model`
// Done in in requestAF() to allow browser to complete the
// native browser click handling of a link
requestAF(() => {
// Update the v-model
// Done in in requestAF() to allow browser to complete the
// native browser click handling of a link
this.currentPage = pageNum
this.$emit('change', pageNum)
this.currentPage = pageNumber
this.$emit('change', pageNumber)
})

// Emulate native link click page reloading behaviour by blurring the
// paginator and returning focus to the document
// Done in a `nextTick()` to ensure rendering complete
this.$nextTick(() => {
// Emulate native link click page reloading behaviour by blurring the
// paginator and returning focus to the document
// Done in a `nextTick()` to ensure rendering complete
const target = evt.currentTarget || evt.target
attemptBlur(target)
})
},
Expand Down
110 changes: 75 additions & 35 deletions src/components/pagination-nav/pagination-nav.spec.js
Expand Up @@ -407,61 +407,101 @@ describe('pagination-nav', () => {
})

it('clicking buttons updates the v-model', async () => {
const wrapper = mount(BPaginationNav, {
propsData: {
baseUrl: '#', // needed to prevent JSDOM errors
numberOfPages: 3,
value: 1,
limit: 10
const App = {
methods: {
onPageClick(bvEvt, page) {
// Prevent 3rd page from being selected
if (page === 3) {
bvEvt.preventDefault()
}
}
},
render(h) {
return h(BPaginationNav, {
props: {
baseUrl: '#', // Needed to prevent JSDOM errors
numberOfPages: 5,
value: 1,
limit: 10
},
on: { 'page-click': this.onPageClick }
})
}
})
expect(wrapper.element.tagName).toBe('NAV')
}

const wrapper = mount(App)
expect(wrapper).toBeDefined()

expect(wrapper.findAll('li').length).toBe(7)
const paginationNav = wrapper.findComponent(BPaginationNav)
expect(paginationNav).toBeDefined()
expect(paginationNav.element.tagName).toBe('NAV')

expect(wrapper.vm.computedCurrentPage).toBe(1)
expect(wrapper.emitted('input')).not.toBeDefined()
// Grab the page links
const lis = paginationNav.findAll('li')
expect(lis.length).toBe(9)

// Click on current page button (does nothing)
await wrapper
.findAll('li')
expect(paginationNav.vm.computedCurrentPage).toBe(1)
expect(paginationNav.emitted('input')).not.toBeDefined()
expect(paginationNav.emitted('change')).not.toBeDefined()
expect(paginationNav.emitted('page-click')).not.toBeDefined()

// Click on current (1st) page link (does nothing)
await lis
.at(2)
.find('a')
.trigger('click')
await waitRAF()
expect(wrapper.vm.computedCurrentPage).toBe(1)
expect(wrapper.emitted('input')).not.toBeDefined()
expect(paginationNav.vm.computedCurrentPage).toBe(1)
expect(paginationNav.emitted('input')).not.toBeDefined()
expect(paginationNav.emitted('change')).not.toBeDefined()
expect(paginationNav.emitted('page-click')).not.toBeDefined()

// Click on 2nd page button
await wrapper
.findAll('li')
// Click on 2nd page link
await lis
.at(3)
.find('a')
.trigger('click')
await waitRAF()
expect(wrapper.vm.computedCurrentPage).toBe(2)
expect(wrapper.emitted('input')).toBeDefined()
expect(wrapper.emitted('input')[0][0]).toBe(2)

// Click goto last button
await wrapper
.findAll('li')
.at(6)
expect(paginationNav.vm.computedCurrentPage).toBe(2)
expect(paginationNav.emitted('input')).toBeDefined()
expect(paginationNav.emitted('change')).toBeDefined()
expect(paginationNav.emitted('page-click')).toBeDefined()
expect(paginationNav.emitted('input')[0][0]).toBe(2)
expect(paginationNav.emitted('change')[0][0]).toBe(2)
expect(paginationNav.emitted('page-click').length).toBe(1)

// Click goto last page link
await lis
.at(8)
.find('a')
.trigger('keydown.space') // Generates a click event
.trigger('click')
await waitRAF()
expect(wrapper.vm.computedCurrentPage).toBe(3)
expect(wrapper.emitted('input')[1][0]).toBe(3)
expect(paginationNav.vm.computedCurrentPage).toBe(5)
expect(paginationNav.emitted('input')[1][0]).toBe(5)
expect(paginationNav.emitted('change')[1][0]).toBe(5)
expect(paginationNav.emitted('page-click').length).toBe(2)

// Click prev button
await wrapper
.findAll('li')
// Click prev page link
await lis
.at(1)
.find('a')
.trigger('click')
await waitRAF()
expect(wrapper.vm.computedCurrentPage).toBe(2)
expect(wrapper.emitted('input')[2][0]).toBe(2)
expect(paginationNav.vm.computedCurrentPage).toBe(4)
expect(paginationNav.emitted('input')[2][0]).toBe(4)
expect(paginationNav.emitted('change')[2][0]).toBe(4)
expect(paginationNav.emitted('page-click').length).toBe(3)

// Click on 3rd page link (prevented)
await lis
.at(4)
.find('a')
.trigger('click')
await waitRAF()
expect(paginationNav.vm.computedCurrentPage).toBe(4)
expect(paginationNav.emitted('input').length).toBe(3)
expect(paginationNav.emitted('change').length).toBe(3)
expect(paginationNav.emitted('page-click').length).toBe(4)

wrapper.destroy()
})
Expand Down
13 changes: 13 additions & 0 deletions src/components/pagination/README.md
Expand Up @@ -364,6 +364,19 @@ By default the pagination component is left aligned. Change the alignment to `ce
<!-- b-pagination-alignment.vue -->
```

## Preventing a page from being selected

You can listen for the `page-click` event, which provides an option to prevent the page from being
selected. The event is emitted with two arguments:

- `bvEvent`: The `BvEvent` object. Call `bvEvt.preventDefault()` to cancel page selection
- `page`: Page number to select (starting with `1`)

For accessibility reasons, when using the `page-click` event to prevent a page from being selected,
you should provide some means of notification to the user as to why the page is not able to be
selected. It is recommended to use the `disabled` attribute on the `<b-pagination>` component
instead of using the `page-click` event (as `disabled` is more intuitive for screen reader users).

## Accessibility

The `<b-pagination>` component provides many features to support assistive technology users, such as
Expand Down
26 changes: 20 additions & 6 deletions src/components/pagination/package.json
Expand Up @@ -138,23 +138,37 @@
"events": [
{
"event": "input",
"description": "when page changes via user interaction or programmatically",
"description": "Emitted when page changes via user interaction or programmatically",
"args": [
{
"arg": "page",
"type": "Number",
"description": "Selected page number (starting with 1)"
"description": "Selected page number (starting with `1`), or `null` if no page found"
}
]
},
{
"event": "change",
"description": "when page changes via user interaction",
"description": "Emitted when page changes via user interaction",
"args": [
{
"arg": "page",
"type": "Number",
"description": "Selected page number (starting with 1)"
"description": "Selected page number (starting with `1`)"
}
]
},
{
"event": "page-click",
"description": "Emitted when a page button was clicked. Cancelable",
"version": "2.17.0",
"args": [
{
"arg": "bvEvt",
"type": "BvEvent",
"description": "The `BvEvent` object. Call `bvEvt.preventDefault()` to cancel page selection"
},
{
"arg": "page",
"description": "Page number to select (starting with `1`)"
}
]
}
Expand Down
42 changes: 27 additions & 15 deletions src/components/pagination/pagination.js
@@ -1,4 +1,5 @@
import Vue from '../../utils/vue'
import { BvEvent } from '../../utils/bv-event.class'
import { getComponentConfig } from '../../utils/config'
import { attemptFocus, isVisible } from '../../utils/dom'
import { isUndefinedOrNull } from '../../utils/inspect'
Expand Down Expand Up @@ -87,8 +88,8 @@ export const BPagination = /*#__PURE__*/ Vue.extend({
this.currentPage = currentPage
} else {
this.$nextTick(() => {
// If this value parses to NaN or a value less than 1
// Trigger an initial emit of 'null' if no page specified
// If this value parses to `NaN` or a value less than `1`
// trigger an initial emit of `null` if no page specified
this.currentPage = 0
})
}
Expand All @@ -99,23 +100,34 @@ export const BPagination = /*#__PURE__*/ Vue.extend({
},
methods: {
// These methods are used by the render function
onClick(num, evt) {
// Handle edge cases where number of pages has changed (i.e. if perPage changes)
// This should normally not happen, but just in case.
if (num > this.numberOfPages) {
/* istanbul ignore next */
num = this.numberOfPages
} else if (num < 1) {
/* istanbul ignore next */
num = 1
onClick(evt, pageNumber) {
// Dont do anything if clicking the current active page
if (pageNumber === this.currentPage) {
return
}
// Update the v-model
this.currentPage = num

const { target } = evt

// Emit a user-cancelable `page-click` event
const clickEvt = new BvEvent('page-click', {
cancelable: true,
vueTarget: this,
target
})
this.$emit(clickEvt.type, clickEvt, pageNumber)
if (clickEvt.defaultPrevented) {
return
}

console.log(evt, pageNumber)

// Update the `v-model`
this.currentPage = pageNumber
// Emit event triggered by user interaction
this.$emit('change', this.currentPage)

// Keep the current button focused if possible
this.$nextTick(() => {
// Keep the current button focused if possible
const target = evt.target
if (isVisible(target) && this.$el.contains(target)) {
attemptFocus(target)
} else {
Expand Down

0 comments on commit 7e18c61

Please sign in to comment.