diff --git a/src/components/form-tags/README.md b/src/components/form-tags/README.md index fd46df3e95f..1fa1ca09100 100644 --- a/src/components/form-tags/README.md +++ b/src/components/form-tags/README.md @@ -1,4 +1,4 @@ -# Form tags +# Form Tags > Lightweight custom tagged input form control, with options for customized interface rendering, > duplicate tag detection and optional tag validation. diff --git a/src/components/pagination-nav/README.md b/src/components/pagination-nav/README.md index 7c198285417..03cbce3f7f0 100644 --- a/src/components/pagination-nav/README.md +++ b/src/components/pagination-nav/README.md @@ -515,23 +515,14 @@ recommended unless the content of the button textually conveys its purpose. ### Keyboard navigation -`` supports keyboard navigation out of the box, and follows the -[WAI-ARIA roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.2/#kbd_roving_tabindex) -pattern. - -- Tabbing into the pagination component will autofocus the current active page button -- LEFT (or UP) and RIGHT (or DOWN) arrow keys will focus - the previous and next buttons, respectively, in the page list -- ENTER or SPACE keys will select (click) the currently focused page button -- Pressing TAB will move to the next control or link on the page, while pressing - SHIFT+TAB will move to the previous control or link on the page. +`` supports standard TAB key navigation. ## See also Refer to the [Router support](/docs/reference/router-links) reference page for router-link specific props. -For pagination control of a component (such as ``), use the +For pagination control of a component (such as ``) or a pagination list, use the [``](/docs/components/pagination) component instead. diff --git a/src/components/pagination-nav/pagination-nav.spec.js b/src/components/pagination-nav/pagination-nav.spec.js index beb80af340d..80697b6755c 100644 --- a/src/components/pagination-nav/pagination-nav.spec.js +++ b/src/components/pagination-nav/pagination-nav.spec.js @@ -32,6 +32,7 @@ describe('pagination-nav', () => { // NAV Attributes expect(wrapper.attributes('aria-hidden')).toBe('false') + expect(wrapper.attributes('aria-label')).toBe('Pagination') // UL Classes expect($ul.classes()).toContain('pagination') @@ -41,9 +42,9 @@ describe('pagination-nav', () => { expect($ul.classes()).not.toContain('justify-content-center') expect($ul.classes()).not.toContain('justify-content-end') // UL Attributes - expect($ul.attributes('role')).toBe('menubar') + expect($ul.attributes('role')).not.toBe('menubar') expect($ul.attributes('aria-disabled')).toBe('false') - expect($ul.attributes('aria-label')).toBe('Pagination') + expect($ul.attributes('aria-label')).not.toBe('Pagination') wrapper.destroy() }) @@ -126,7 +127,7 @@ describe('pagination-nav', () => { expect($ul.classes()).toContain('b-pagination') // UL Attributes - expect($ul.attributes('role')).toBe('menubar') + expect($ul.attributes('role')).not.toBe('menubar') expect($ul.attributes('aria-disabled')).toBe('true') // LI classes diff --git a/src/components/pagination/pagination.js b/src/components/pagination/pagination.js index 94c659b2d8c..ad6f0fc976b 100644 --- a/src/components/pagination/pagination.js +++ b/src/components/pagination/pagination.js @@ -132,8 +132,8 @@ export const BPagination = /*#__PURE__*/ Vue.extend({ return pageNum }, linkProps() { - // Always '#' for pagination component - return { href: '#' } + // No props, since we render a plain button + return {} } } }) diff --git a/src/components/pagination/pagination.spec.js b/src/components/pagination/pagination.spec.js index 52d2b4c9bfd..91fc6409073 100644 --- a/src/components/pagination/pagination.spec.js +++ b/src/components/pagination/pagination.spec.js @@ -56,7 +56,7 @@ describe('pagination', () => { if (index === 2) { expect(li.classes()).toContain('active') expect(li.classes()).not.toContain('disabled') - expect(pageLink.is('a')).toBe(true) + expect(pageLink.is('button')).toBe(true) } else { expect(li.classes()).not.toContain('active') expect(li.classes()).toContain('disabled') @@ -78,14 +78,13 @@ describe('pagination', () => { expect(last.find('.page-link').text()).toEqual('ยป') // Page button attrs - expect(page.find('.page-link').attributes('href')).toEqual('#') + expect(page.find('.page-link').attributes('type')).toEqual('button') expect(page.find('.page-link').attributes('role')).toEqual('menuitemradio') expect(page.find('.page-link').attributes('aria-checked')).toEqual('true') expect(page.find('.page-link').attributes('aria-posinset')).toEqual('1') expect(page.find('.page-link').attributes('aria-setsize')).toEqual('1') expect(page.find('.page-link').attributes('tabindex')).toEqual('0') expect(page.find('.page-link').attributes('aria-label')).toEqual('Go to page 1') - expect(page.find('.page-link').attributes('target')).toEqual('_self') wrapper.destroy() }) @@ -133,7 +132,7 @@ describe('pagination', () => { disabled: false }) - const $links = wrapper.findAll('a.page-link') + const $links = wrapper.findAll('button.page-link') expect($links.length).toBe(5) expect($links.at(0).text()).toBe('Page 1') expect($links.at(1).text()).toBe('Page 2') @@ -388,16 +387,16 @@ describe('pagination', () => { }) expect(wrapper.is('ul')).toBe(true) expect(wrapper.findAll('li').length).toBe(5) - expect(wrapper.findAll('a.page-link').length).toBe(4) - expect(wrapper.findAll('a.page-link').is('[aria-controls="foo"]')).toBe(true) + expect(wrapper.findAll('button.page-link').length).toBe(4) + expect(wrapper.findAll('button.page-link').is('[aria-controls="foo"]')).toBe(true) wrapper.setProps({ ariaControls: null }) await waitNT(wrapper.vm) expect(wrapper.findAll('li').length).toBe(5) - expect(wrapper.findAll('a.page-link').length).toBe(4) - expect(wrapper.findAll('a.page-link').is('[aria-controls]')).toBe(false) + expect(wrapper.findAll('button.page-link').length).toBe(4) + expect(wrapper.findAll('button.page-link').is('[aria-controls]')).toBe(false) wrapper.destroy() }) @@ -414,28 +413,28 @@ describe('pagination', () => { }) expect(wrapper.is('ul')).toBe(true) expect(wrapper.findAll('li').length).toBe(5) - expect(wrapper.findAll('a').length).toBe(4) + expect(wrapper.findAll('button').length).toBe(4) expect( wrapper - .findAll('a') + .findAll('button') .at(0) .attributes('aria-label') ).toBe('Go to page 1') expect( wrapper - .findAll('a') + .findAll('button') .at(1) .attributes('aria-label') ).toBe('Go to page 2') expect( wrapper - .findAll('a') + .findAll('button') .at(2) .attributes('aria-label') ).toBe('Go to page 3') expect( wrapper - .findAll('a') + .findAll('button') .at(3) .attributes('aria-label') ).toBe('Go to next page') @@ -654,7 +653,7 @@ describe('pagination', () => { wrapper .findAll('li') .at(3) - .find('a') + .find('button') .trigger('click') await waitNT(wrapper.vm) expect(wrapper.vm.computedCurrentPage).toBe(2) @@ -667,7 +666,7 @@ describe('pagination', () => { wrapper .findAll('li') .at(6) - .find('a') + .find('button') .trigger('keydown.space') // Generates a click event await waitNT(wrapper.vm) expect(wrapper.vm.computedCurrentPage).toBe(3) @@ -678,7 +677,7 @@ describe('pagination', () => { wrapper .findAll('li') .at(1) - .find('a') + .find('button') .trigger('click') await waitNT(wrapper.vm) expect(wrapper.vm.computedCurrentPage).toBe(2) @@ -1031,7 +1030,7 @@ describe('pagination', () => { expect(wrapper.is('ul')).toBe(true) await waitNT(wrapper.vm) // Grab the button links (2 bookends + 3 pages + 2 bookends) - const links = wrapper.findAll('a.page-link') + const links = wrapper.findAll('button.page-link') expect(links.length).toBe(7) // Sanity check for getBCR override @@ -1091,7 +1090,7 @@ describe('pagination', () => { await waitNT(wrapper.vm) expect(wrapper.is('ul')).toBe(true) // Grab the button links (2 bookends + 3 pages + 2 bookends) - const links = wrapper.findAll('a.page-link') + const links = wrapper.findAll('button.page-link') expect(links.length).toBe(7) // Focus the last button @@ -1121,14 +1120,14 @@ describe('pagination', () => { await waitNT(wrapper.vm) expect(wrapper.is('ul')).toBe(true) // Grab the button links (2 disabled bookends + 4 pages + (-ellipsis) + 2 bookends) - links = wrapper.findAll('a.page-link') + links = wrapper.findAll('button.page-link') expect(links.length).toBe(6) // Click on the 4th button (page 4, index 3) links.at(3).element.click() await waitNT(wrapper.vm) // Links re-rendered with first bookends enabled and an ellipsis - links = wrapper.findAll('a.page-link') + links = wrapper.findAll('button.page-link') // The 4th link should be page 4, and retain focus expect(document.activeElement).toEqual(links.at(3).element) diff --git a/src/mixins/pagination.js b/src/mixins/pagination.js index 81e5d4da46e..8746873f075 100644 --- a/src/mixins/pagination.js +++ b/src/mixins/pagination.js @@ -351,6 +351,11 @@ export default { methods: { handleKeyNav(evt) { const { keyCode, shiftKey } = evt + /* istanbul ignore if */ + if (this.isNav) { + // We disable left/right keyboard navigation in `` + return + } if (keyCode === KeyCodes.LEFT || keyCode === KeyCodes.UP) { evt.preventDefault() shiftKey ? this.focusFirst() : this.focusPrev() @@ -361,7 +366,7 @@ export default { }, getButtons() { // Return only buttons that are visible - return selectAll('a.page-link', this.$el).filter(btn => isVisible(btn)) + return selectAll('button.page-link, a.page-link', this.$el).filter(btn => isVisible(btn)) }, setBtnFocus(btn) { btn.focus() @@ -430,26 +435,30 @@ export default { const { showFirstDots, showLastDots } = this.paginationParams const currentPage = this.computedCurrentPage const fill = this.align === 'fill' + // Used to control what type of aria attributes are rendered and wrapper + const isNav = this.isNav // Helper function and flag - const isActivePage = pageNum => pageNum === currentPage + const isActivePage = pageNumber => pageNumber === currentPage const noCurrentPage = this.currentPage < 1 // Factory function for prev/next/first/last buttons const makeEndBtn = (linkTo, ariaLabel, btnSlot, btnText, btnClass, pageTest, key) => { const isDisabled = disabled || isActivePage(pageTest) || noCurrentPage || linkTo < 1 || linkTo > numberOfPages - const pageNum = linkTo < 1 ? 1 : linkTo > numberOfPages ? numberOfPages : linkTo - const scope = { disabled: isDisabled, page: pageNum, index: pageNum - 1 } - const btnContent = this.normalizeSlot(btnSlot, scope) || toString(btnText) || h() - const inner = h( - isDisabled ? 'span' : BLink, + const pageNumber = linkTo < 1 ? 1 : linkTo > numberOfPages ? numberOfPages : linkTo + const scope = { disabled: isDisabled, page: pageNumber, index: pageNumber - 1 } + const $btnContent = this.normalizeSlot(btnSlot, scope) || toString(btnText) || h() + const $inner = h( + isDisabled ? 'span' : isNav ? BLink : 'button', { staticClass: 'page-link', - props: isDisabled ? {} : this.linkProps(linkTo), + class: { 'flex-grow-1': !isNav && !isDisabled && fill }, + props: isDisabled || !isNav ? {} : this.linkProps(linkTo), attrs: { - role: 'menuitem', - tabindex: isDisabled ? null : '-1', + role: isNav ? null : 'menuitem', + type: isNav || isDisabled ? null : 'button', + tabindex: isDisabled || isNav ? null : '-1', 'aria-label': ariaLabel, 'aria-controls': this.ariaControls || null, 'aria-disabled': isDisabled ? 'true' : null @@ -457,26 +466,33 @@ export default { on: isDisabled ? {} : { - click: evt => { + '!click': evt => { this.onClick(linkTo, evt) }, keydown: onSpaceKey } }, - [btnContent] + [$btnContent] ) return h( 'li', { key, staticClass: 'page-item', - class: [{ disabled: isDisabled, 'flex-fill': fill }, btnClass], + class: [ + { + disabled: isDisabled, + 'flex-fill': fill, + 'd-flex': fill && !isNav && !isDisabled + }, + btnClass + ], attrs: { - role: 'presentation', + role: isNav ? null : 'presentation', 'aria-hidden': isDisabled ? 'true' : null } }, - [inner] + [$inner] ) } @@ -504,17 +520,19 @@ export default { // Active page will have tabindex of 0, or if no current page and first page button const tabIndex = disabled ? null : active || (noCurrentPage && idx === 0) ? '0' : '-1' const attrs = { - role: 'menuitemradio', + role: isNav ? null : 'menuitemradio', + type: isNav || disabled ? null : 'button', 'aria-disabled': disabled ? 'true' : null, 'aria-controls': this.ariaControls || null, 'aria-label': isFunction(this.labelPage) ? this.labelPage(page.number) : `${this.labelPage} ${page.number}`, - 'aria-checked': active ? 'true' : 'false', + 'aria-checked': isNav ? null : active ? 'true' : 'false', + 'aria-current': isNav && active ? 'page' : null, 'aria-posinset': page.number, 'aria-setsize': numberOfPages, - // ARIA "roving tabindex" method - tabindex: tabIndex + // ARIA "roving tabindex" method (except in isNav mode) + tabindex: isNav ? null : tabIndex } const btnContent = toString(this.makePage(page.number)) const scope = { @@ -524,16 +542,17 @@ export default { active, disabled } - const inner = h( - disabled ? 'span' : BLink, + const $inner = h( + disabled ? 'span' : isNav ? BLink : 'button', { - props: disabled ? {} : this.linkProps(page.number), + props: disabled || !isNav ? {} : this.linkProps(page.number), staticClass: 'page-link', + class: { 'flex-grow-1': !isNav && !disabled && fill }, attrs, on: disabled ? {} : { - click: evt => { + '!click': evt => { this.onClick(page.number, evt) }, keydown: onSpaceKey @@ -546,10 +565,19 @@ export default { { key: `page-${page.number}`, staticClass: 'page-item', - class: [{ disabled, active, 'flex-fill': fill }, page.classes, this.pageClass], - attrs: { role: 'presentation' } + class: [ + { + disabled, + active, + 'flex-fill': fill, + 'd-flex': fill && !isNav && !disabled + }, + page.classes, + this.pageClass + ], + attrs: { role: isNav ? null : 'presentation' } }, - [inner] + [$inner] ) } @@ -641,23 +669,25 @@ export default { staticClass: 'pagination', class: ['b-pagination', this.btnSize, this.alignment, this.styleClass], attrs: { - role: 'menubar', + role: isNav ? null : 'menubar', 'aria-disabled': disabled ? 'true' : 'false', - 'aria-label': this.ariaLabel || null + 'aria-label': isNav ? null : this.ariaLabel || null }, - on: { keydown: this.handleKeyNav } + // We disable keyboard left/right nav when `` + on: isNav ? {} : { keydown: this.handleKeyNav } }, buttons ) // If we are ``, wrap in `