diff --git a/packages/locale/lang/en.ts b/packages/locale/lang/en.ts index 77b060cdc2..17bcefac1b 100644 --- a/packages/locale/lang/en.ts +++ b/packages/locale/lang/en.ts @@ -76,6 +76,7 @@ export default { pagesize: '/page', total: 'Total {total}', pageClassifier: '', + deprecationWarning: 'Deprecated usages detected, please refer to the el-pagination documentation for more details', }, messagebox: { title: 'Message', diff --git a/packages/locale/lang/zh-cn.ts b/packages/locale/lang/zh-cn.ts index e36acd12b8..3bfed70781 100644 --- a/packages/locale/lang/zh-cn.ts +++ b/packages/locale/lang/zh-cn.ts @@ -76,6 +76,7 @@ export default { pagesize: '条/页', total: '共 {total} 条', pageClassifier: '页', + deprecationWarning: '你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档', }, messagebox: { title: '提示', diff --git a/packages/pagination/__tests__/pagination.spec.ts b/packages/pagination/__tests__/pagination.spec.ts index 49721ed657..769991fd89 100644 --- a/packages/pagination/__tests__/pagination.spec.ts +++ b/packages/pagination/__tests__/pagination.spec.ts @@ -1,407 +1,371 @@ -import { mount } from '@vue/test-utils' -import { sleep } from '@element-plus/test-utils' +import { mount, VueWrapper } from '@vue/test-utils' import Pagination from '../src/index' import { nextTick, ref, h } from 'vue' -const TIME_OUT = 100 +const assertElementsExistence = (wrapper: VueWrapper, selectors: string[], existence: boolean) => { + selectors.forEach(selector => { + expect(wrapper.find(selector).exists()).toBe(existence) + }) +} -describe('Pagination.vue', () => { - test('layout', () => { - const wrapper = mount(Pagination, { - props: { - layout: 'prev, pager, next', - }, +const assertCurrent = (wrapper, page) => { + expect(wrapper.find('.el-pager li.active.number').text()).toBe(String(page)) +} +const assertPages = (wrapper, total) => { + expect(wrapper.find('.el-pagination .el-pager li:last-child').text()).toBe(String(total)) +} + +describe('Pagination', () => { + describe('test invalid usages', () => { + const cacheWarn = console.warn + beforeEach(() => { + console.warn = jest.fn() + }) + afterEach(() => { + console.warn = cacheWarn + }) + test('both absence of total & pageCount is invalid', async () => { + expect(console.warn).not.toHaveBeenCalled() + const total = ref(undefined) + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { total: total.value }) + } + }, + }) + expect(wrapper.find('.el-pagination').exists()).toBe(false) + expect(console.warn).toHaveBeenCalled() + total.value = 100 + await nextTick() + expect(wrapper.find('.el-pagination').exists()).toBe(true) + }) + test('current-page defined while absence of current-page listener is invalid', () => { + expect(console.warn).not.toHaveBeenCalled() + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + currentPage: 1, + }) + } + }, + }) + expect(wrapper.find('.el-pagination').exists()).toBe(false) + expect(console.warn).toHaveBeenCalled() + }) + test('layout with `sizes` restrictions(page-count)', () => { + expect(console.warn).not.toHaveBeenCalled() + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + layout: 'sizes, pager', + pageCount: 10, + }) + } + }, + }) + expect(wrapper.find('.el-pagination').exists()).toBe(false) + expect(console.warn).toHaveBeenCalled() + }) + test('layout with `sizes` restrictions(page-size)', () => { + expect(console.warn).not.toHaveBeenCalled() + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + layout: 'sizes, pager', + pageSize: 10, + }) + } + }, + }) + expect(wrapper.find('.el-pagination').exists()).toBe(false) + expect(console.warn).toHaveBeenCalled() }) - expect(wrapper.find('button.btn-prev').exists()).toBe(true) - expect(wrapper.find('ul.el-pager').exists()).toBe(true) - expect(wrapper.find('button.btn-next').exists()).toBe(true) - expect(wrapper.find('.el-pagination__jump').exists()).toBe(false) - expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(false) - expect(wrapper.find('.el-pagination__total').exists()).toBe(false) }) - test('change layout value', async () => { - const layout = ref('prev, pager, next') - - const Comp = { + describe('test layout & layout reactive change', () => { + const layoutRef = ref('') + const wrapper = mount({ setup() { return () => { return h(Pagination, { - class: 'pagination-wrapper', - layout: layout.value, + total: 100, + layout: layoutRef.value, }) } }, - } - const wrapper = mount(Comp) - - expect(wrapper.find('button.btn-prev').exists()).toBe(true) - expect(wrapper.find('ul.el-pager').exists()).toBe(true) - expect(wrapper.find('button.btn-next').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(false) - - layout.value = 'prev, pager, next, ->' - await nextTick() - expect(wrapper.find('button.btn-prev').exists()).toBe(true) - expect(wrapper.find('ul.el-pager').exists()).toBe(true) - expect(wrapper.find('button.btn-next').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(false) - - layout.value = 'prev, pager, next, ->, jumper' - await nextTick() - expect(wrapper.find('button.btn-prev').exists()).toBe(true) - expect(wrapper.find('ul.el-pager').exists()).toBe(true) - expect(wrapper.find('button.btn-next').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__jump').exists()).toBe(true) - - layout.value = 'prev, pager, next, ->, jumper, sizes' - await nextTick() - expect(wrapper.find('button.btn-prev').exists()).toBe(true) - expect(wrapper.find('ul.el-pager').exists()).toBe(true) - expect(wrapper.find('button.btn-next').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__jump').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__sizes').exists()).toBe(true) - - layout.value = 'prev, pager, next, ->, jumper, sizes, total' - await nextTick() - expect(wrapper.find('button.btn-prev').exists()).toBe(true) - expect(wrapper.find('ul.el-pager').exists()).toBe(true) - expect(wrapper.find('button.btn-next').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__jump').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__sizes').exists()).toBe(true) - expect(wrapper.find('.el-pagination__rightwrapper .el-pagination__total').exists()).toBe(true) - }) - - test('slot', () => { - const TestComponent = { - template: ` - - slot test - - `, - components: { - 'el-pagination': Pagination, - }, - } - const wrapper = mount(TestComponent) - expect(wrapper.find('.slot-test').exists()).toBe(true) - }) - - test('small', () => { - const wrapper = mount(Pagination, { - props: { - small: true, - }, }) - expect(wrapper.vm.$el.classList.contains('el-pagination--small')).toBe(true) - }) - - test('pageSize', () => { - const wrapper = mount(Pagination, { - props: { - pageSize: 25, - total: 100, - }, + test('layout empty', async () => { + await nextTick() + expect(wrapper.find('.el-pagination').exists()).toBe(false) }) - expect(wrapper.findAll('li.number').length).toBe(4) - }) - - test('pageSize: NaN', () => { - const wrapper = mount(Pagination, { - props: { - pageSize: NaN, - total: 100, - }, - }) - expect(wrapper.findAll('li.number').length).toBe(7) - }) - - test('pageCount', () => { - const wrapper = mount(Pagination, { - props: { - pageSize: 25, - pagerCount: 5, - pageCount: 50, - }, - }) - expect(wrapper.findAll('li.number').length).toBe(5) - }) - - test('pagerCount', () => { - const wrapper = mount(Pagination, { - props: { - pageSize: 25, - total: 1000, - pagerCount: 21, - }, - }) - expect(wrapper.findAll('li.number').length).toBe(21) - }) - - test('will work without total & page-count', async () => { - const wrapper = mount(Pagination, { - props: { - pageSize: 25, - currentPage: 2, - }, - }) - wrapper.find('.btn-prev').trigger('click') - await sleep(TIME_OUT) - expect(wrapper.vm.internalCurrentPage).toEqual(1) - wrapper.find('.btn-prev').trigger('click') - expect(wrapper.vm.internalCurrentPage).toEqual(1) - }) - - test('currentPage', () => { - const wrapper = mount(Pagination, { - props: { - pageSize: 20, - total: 200, - currentPage: 3, - }, - }) - expect(wrapper.find('li.number.active').text()).toEqual('3') - }) - - test('currentPage: NaN', () => { - const wrapper = mount(Pagination, { - props: { - pageSize: 20, - total: 200, - currentPage: NaN, - }, - }) - expect(wrapper.find('li.number.active').text()).toEqual('1') - expect(wrapper.vm.$el.querySelectorAll('li.number').length).toBe(7) - }) - - test('layout is empty', () => { - const wrapper = mount(Pagination, { - props: { - layout: '', - }, - }) - expect(wrapper.vm.$el.textContent).toEqual('') - }) -}) - -describe('click pager', () => { - test('click ul', () => { - const wrapper = mount(Pagination, { - props: { - total: 1000, - }, - }) - wrapper.find('.el-pager').trigger('click') - expect(wrapper.vm.internalCurrentPage).toEqual(1) - }) - - test('click li', () => { - const wrapper = mount(Pagination, { - props: { - total: 1000, - }, - }) - wrapper.findAll('.el-pager li.number')[1].trigger('click') - expect(wrapper.vm.internalCurrentPage).toEqual(2) - }) - - test('click next icon-more', () => { - const wrapper = mount(Pagination, { - props: { - total: 1000, - }, - }) - wrapper.find('.btn-quicknext.more').trigger('click') - expect(wrapper.vm.internalCurrentPage).toEqual(6) - }) - - test('click prev icon-more', async () => { - const wrapper = mount(Pagination, { - props: { - total: 1000, - }, - }) - wrapper.find('.btn-quicknext.more').trigger('click') - await sleep(TIME_OUT) - expect(wrapper.find('.btn-quickprev.more').exists()).toBe(true) - wrapper.find('.btn-quickprev.more').trigger('click') - expect(wrapper.vm.internalCurrentPage).toEqual(1) - }) - - test('click last page', async () => { - const wrapper = mount(Pagination, { - props: { - total: 1000, - }, - }) - const nodes = wrapper.findAll('li.number') - nodes[nodes.length - 1].trigger('click') - await sleep(TIME_OUT) - expect(wrapper.find('.btn-quickprev.more').exists()).toBe(true) - expect(wrapper.find('.btn-quicknext.more').exists()).toBe(false) - }) - - test('should emit change size evt and update pageSize', async () => { - const onSizeChange = jest.fn() - const wrapper = mount({ - components: { - 'el-pagination': Pagination, - }, - template: ` - - `, - methods: { - onSizeChange, - }, - data(){ - return { - pageSize: 200, + const layoutSelectorPairs = [ + ['sizes', '.el-pagination__sizes'], + ['prev', 'button.btn-prev'], + ['pager', 'ul.el-pager'], + ['next', 'button.btn-next'], + ['jumper', '.el-pagination__jump'], + ['total', '.el-pagination__total'], + ] + layoutSelectorPairs.forEach(([layout], idx) => { + test(`layout with only '${layout}'`, async () => { + layoutRef.value = layout + await nextTick() + for(let i = 0; i < layoutSelectorPairs.length; i++) { + expect(wrapper.find(layoutSelectorPairs[i][1]).exists()).toBe(i === idx) } - }, + }) }) - const items = document.querySelector('.select-dropdown-klass').querySelectorAll('.el-select-dropdown__item:not(.selected)'); - (items[0] as HTMLOptionElement)?.click() - expect(onSizeChange).toHaveBeenCalled() - expect(wrapper.vm.pageSize).toBe(100) - expect(wrapper.findComponent(Pagination).emitted()).toHaveProperty('size-change') - }) - - - test('should handle total size change', async () => { - const onCurrentChange = jest.fn() - const wrapper = mount({ - components: { - [Pagination.name]: Pagination, - }, - template: ` - - }, - `, - methods: { - onCurrentChange, - }, - data() { - return { - currentPage: 3, - total: 1000, - pageSize: 100, - } - }, + test(`layout with '->, total'`, async () => { + layoutRef.value = '->, total' + await nextTick() + assertElementsExistence(wrapper, ['.el-pagination__total', '.el-pagination__rightwrapper'], true) }) - await nextTick() + test('layout with default layout prop', () => { + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + }) + } + }, + }) + assertElementsExistence(wrapper, [ + '.el-pagination__rightwrapper', + 'button.btn-prev', + 'ul.el-pager', + 'button.btn-next', + '.el-pagination__jump', + ], true) + }) - expect(wrapper.vm.currentPage).toBe(3) - - wrapper.vm.total = 100 - await nextTick() - expect(wrapper.vm.currentPage).toBe(1) - expect(onCurrentChange).toHaveBeenCalledWith(1) - }) - -}) - -test('repeat click next & change current page', async () => { - const onCurrentChange = jest.fn() - const wrapper = mount({ - components: { - [Pagination.name]: Pagination, - }, - template: ` - - }, - `, - methods: { - onCurrentChange, - }, - data() { - return { - currentPage: 1, - total: 400, - pageSize: 100, + test('test layout with slot', () => { + const TestComponent = { + template: ` + + slot test + + `, + components: { + 'el-pagination': Pagination, + }, } - }, + const wrapper = mount(TestComponent) + expect(wrapper.find('.slot-test').exists()).toBe(true) + }) + + test('test small layout', () => { + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + small: true, + }) + } + }, + }) + expect(wrapper.vm.$el.classList.contains('el-pagination--small')).toBe(true) + }) + + test('test with background', async () => { + const withBackground = ref(true) + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + background: withBackground.value, + }) + } + }, + }) + expect(wrapper.find('.is-background').exists()).toBe(true) + withBackground.value = false + await nextTick() + expect(wrapper.find('.is-background').exists()).toBe(false) + }) + + test('test hide-on-single-page prop', async () => { + const hideOnSinglePage = ref(false) + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 10, // deivded by default page-size(10), there will be only one page + hideOnSinglePage: hideOnSinglePage.value, + }) + } + }, + }) + expect(wrapper.find('.el-pagination').exists()).toBe(true) + hideOnSinglePage.value = true + await nextTick() + expect(wrapper.find('.el-pagination').exists()).toBe(false) + }) }) - await nextTick() + describe('test pageSize & currentPage reactive change', () => { + test(`test pageSize change`, async () => { + const pageSize = ref(10) + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + layout: 'pager', + total: 100, + pageSize: pageSize.value, + }) + } + }, + }) + // total pages = Math.ceil(total / pageSize) + assertPages(wrapper, 10) + pageSize.value = 20 + await nextTick() + assertPages(wrapper, 5) + pageSize.value = 55 + await nextTick() + assertPages(wrapper, 2) + }) + test('test currentPage change', async () => { + const pageSize = ref(10) + const defaultCurrentPage = ref(2) + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + layout: 'prev, pager, next', + total: 100, + pageSize: pageSize.value, + defaultCurrentPage: defaultCurrentPage.value, + }) + } + }, + }) + assertCurrent(wrapper, 2) + defaultCurrentPage.value = 1 + assertCurrent(wrapper, 2) // still 2 + await wrapper.find('.el-pager li:last-child').trigger('click') + assertCurrent(wrapper, 10) + await wrapper.find('button.btn-prev').trigger('click') + assertCurrent(wrapper, 9) + await wrapper.find('button.btn-next').trigger('click') + assertCurrent(wrapper, 10) + pageSize.value = 50 + await nextTick() + assertCurrent(wrapper, 2) + }) - expect(wrapper.vm.currentPage).toBe(1) - wrapper.find('.btn-next').trigger('click') - await nextTick() - expect(wrapper.vm.currentPage).toBe(2) - wrapper.vm.currentPage = 1 - await nextTick() - expect(wrapper.vm.currentPage).toBe(1) - wrapper.find('.btn-next').trigger('click') - await nextTick() - expect(wrapper.vm.currentPage).toBe(2) -}) + test('test pageCount change and side effect', async () => { + const pageCount = ref(10) + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + layout: 'prev, pager, next', + pageCount: pageCount.value, + }) + } + }, + }) + assertPages(wrapper, 10) + pageCount.value = 20 + await nextTick() + assertPages(wrapper, 20) + await wrapper.find('.el-pager li:last-child').trigger('click') + assertCurrent(wrapper, 20) + pageCount.value = 5 + await nextTick() + // side effect, if currentPage is greater than pageCount + // currentPage should change accordingly + assertPages(wrapper, 5) + assertCurrent(wrapper, 5) + }) -test('repeat click prev & change current page', async () => { - const onCurrentChange = jest.fn() - const wrapper = mount({ - components: { - [Pagination.name]: Pagination, - }, - template: ` - - }, - `, - methods: { - onCurrentChange, - }, - data() { - return { - currentPage: 2, - total: 400, - pageSize: 100, - } - }, + test('test listener work', async () => { + const pageSizeWatcher = jest.fn() + const currentPageWatcher = jest.fn() + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + layout: 'prev, pager, next, sizes', + ['onUpdate:currentPage']: currentPageWatcher, + ['onUpdate:pageSize']: pageSizeWatcher, + }) + } + }, + }) + await wrapper.find('.el-pager li:last-child').trigger('click') + assertCurrent(wrapper, 10 /* Math.ceil(100/10) */) + expect(currentPageWatcher).toHaveBeenCalled() + await wrapper.find('.el-select').trigger('click') + await wrapper.getComponent('.el-select-dropdown').find('li:nth-child(2)').trigger('click') + expect(pageSizeWatcher).toHaveBeenCalled() + assertCurrent(wrapper, 5/* Math.ceil(100/20) */) + }) }) - await nextTick() + describe('test a11y supports', () => { + test('test a11y attributes', async () => { + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + }) + } + }, + }) + expect(wrapper.find('.el-pagination').attributes('aria-label')).toBe('pagination') + expect(wrapper.find('.el-pagination').attributes('role')).toBe('pagination') + expect(wrapper.find('.el-pagination .btn-prev').attributes('aria-disabled')).toBe('true') + expect(wrapper.find('.el-pagination .btn-next').attributes('aria-disabled')).toBe('false') + expect(wrapper.find('.el-pager li:first-child').attributes('aria-current')).toBe('true') + expect(wrapper.find('.el-pager li:last-child').attributes('aria-current')).toBe('false') + await wrapper.find('.el-pager li:last-child').trigger('click') + expect(wrapper.find('.el-pagination .btn-prev').attributes('aria-disabled')).toBe('false') + expect(wrapper.find('.el-pagination .btn-next').attributes('aria-disabled')).toBe('true') + expect(wrapper.find('.el-pager li:first-child').attributes('aria-current')).toBe('false') + expect(wrapper.find('.el-pager li:last-child').attributes('aria-current')).toBe('true') + }) - expect(wrapper.vm.currentPage).toBe(2) - wrapper.find('.btn-prev').trigger('click') - await nextTick() - expect(wrapper.vm.currentPage).toBe(1) - wrapper.vm.currentPage = 2 - await nextTick() - expect(wrapper.vm.currentPage).toBe(2) - wrapper.find('.btn-prev').trigger('click') - await nextTick() - expect(wrapper.vm.currentPage).toBe(1) + test('test tabindex interactive', async () => { + const wrapper = mount({ + setup() { + return () => { + return h(Pagination, { + total: 100, + }) + } + }, + }) + await wrapper.find('.el-pager li:nth-child(2)').trigger('click') + assertCurrent(wrapper, 2) + await wrapper.find('.el-pager li:nth-child(3)').trigger('click', { + key: 'Enter', + }) + assertCurrent(wrapper, 3) + // TODO getComputedStyle is not implemented in jsdom, so I duno how to assert style of psuedo-class + /* + * await wrapper.find('.el-pager li:nth-child(3)').trigger('keyup', { + * key: 'Tab', + * }) + * const style = window.getComputedStyle(wrapper.find('.el-pager li:nth-child(4)').element, ':focus-visible') + * expect(style.outline).toBeTruthy() + */ + }) + }) }) - diff --git a/packages/pagination/src/index.ts b/packages/pagination/src/index.ts index e14d5ae2e1..d02fd53685 100644 --- a/packages/pagination/src/index.ts +++ b/packages/pagination/src/index.ts @@ -1,11 +1,13 @@ import { - defineComponent, h, ref, - computed, - watch, provide, + computed, + defineComponent, + getCurrentInstance, } from 'vue' +import { warn } from '@element-plus/utils/error' +import { t } from '@element-plus/locale' import { IPagination } from './pagination' import Prev from './prev.vue' @@ -15,10 +17,17 @@ import Jumper from './jumper.vue' import Total from './total.vue' import Pager from './pager.vue' -const getValidPageSize = (val: number) => Number.isNaN(val) ? 10 : val +/** + * It it user's responsibility to guarantee that the value of props.total... is number + * (same as pageSize, defaultPageSize, currentPage, defaultCurrentPage, pageCount) + * Otherwise we can reasonable infer that the corresponding field is absent + */ +const isAbsent = v => typeof v !== 'number' + +const componentName = 'ElPagination' export default defineComponent({ - name: 'ElPagination', + name: componentName, components: { Prev, @@ -29,14 +38,23 @@ export default defineComponent({ Pager, }, props: { - pageSize: { + total: { type: Number, - default: 10, }, - small: Boolean, + pageSize: { + type: Number, + }, - total: { + defaultPageSize: { + type: Number, + }, + + currentPage: { + type: Number, + }, + + defaultCurrentPage: { type: Number, }, @@ -54,11 +72,6 @@ export default defineComponent({ default: 7, }, - currentPage: { - type: Number, - default: 1, - }, - layout: { type: String, default: 'prev, pager, next, jumper, ->, total', @@ -86,6 +99,8 @@ export default defineComponent({ default: '', }, + small: Boolean, + background: Boolean, disabled: Boolean, @@ -94,201 +109,213 @@ export default defineComponent({ }, emits: [ + 'update:current-page', + 'update:page-size', + // events below are depracated + // v-model:current-page and v-model:page-size are better choices 'size-change', 'current-change', 'prev-click', 'next-click', - 'update:currentPage', - 'update:pageSize', ], - setup(props, { emit }) { - const lastEmittedPage = ref(-1) - const userChangePageSize = ref(false) - const internalPageSize = ref(getValidPageSize(props.pageSize)) - - const internalPageCount = computed>(() => { - if (typeof props.total === 'number') { - return Math.max(1, Math.ceil(props.total / internalPageSize.value)) - } else if (typeof props.pageCount === 'number') { - return Math.max(1, props.pageCount) + setup(props, { emit, slots }) { + const vnodeProps = getCurrentInstance().vnode.props || {} + // we can find @xxx="xxx" props on `vnodeProps` to check if user bind corresponding events + const hasCurrentPageListener = 'onUpdate:currentPage' in vnodeProps || 'onCurrentChange' in vnodeProps + const hasPageSizeListener = 'onUpdate:pageSize' in vnodeProps || 'onSizeChange' in vnodeProps + const assertValidUsage = computed(() => { + // Users have to set either one, otherwise count of pages cannot be determined + if (isAbsent(props.total) && isAbsent(props.pageCount)) return false + // without corresponding listener is forbidden now + // Users have to use two way binding of `currentPage` + // If users just want to provide a default value, `defaultCurrentPage` is here for you + if (!isAbsent(props.currentPage) && !hasCurrentPageListener) return false + // When you want to change sizes, things get more complex, detailed below + // Basically the most important value we need is page count + // either directly from props.pageCount + // or calculated from props.total + // we will take props.pageCount precedence over props.total + if (props.layout.includes('sizes')) { + if (!isAbsent(props.pageCount)) { + // if props.pageCount is assign by user, then user have to watch pageSize change + // and recalculate pageCount + if (!hasPageSizeListener) return false + } else if (!isAbsent(props.total)) { + // Otherwise, we will see if user have props.pageSize defined + // If so, meaning user want to have pageSize controlled himself/herself from component + // Thus page size listener is required + // users are account for page size change + if (!isAbsent(props.pageSize)) { + if (!hasPageSizeListener) { + return false + } + } else { + // (else block just for explaination) + // else page size is controlled by el-pagination internally + } + } } - return null + return true }) - const internalCurrentPage = ref(getValidCurrentPage(props.currentPage)) + const innerPageSize = ref(isAbsent(props.defaultPageSize) ? 10 : props.defaultPageSize) + const innerCurrentPage = ref(isAbsent(props.defaultCurrentPage) ? 1 : props.defaultCurrentPage) - function emitChange() { - if ( - internalCurrentPage.value !== lastEmittedPage.value || - userChangePageSize.value - ) { - lastEmittedPage.value = internalCurrentPage.value - userChangePageSize.value = false - emit('update:currentPage', internalCurrentPage.value) - emit('current-change', internalCurrentPage.value) + const pageSizeBridge = computed({ + get() { + return isAbsent(props.pageSize) ? innerPageSize.value : props.pageSize + }, + set(v: number) { + if (isAbsent(props.pageSize)) { + innerPageSize.value = v + } + if (hasPageSizeListener) { + emit('update:page-size', v) + emit('size-change', v) + } + }, + }) + + const pageCountBridge = computed(() => { + let pageCount = 0 + if (!isAbsent(props.pageCount)) { + pageCount = props.pageCount + } else if (!isAbsent(props.total)) { + pageCount = Math.max(1, Math.ceil(props.total / pageSizeBridge.value)) } - } + // side effect + if (currentPageBridge.value > pageCount) { + currentPageBridge.value = pageCount + } + return pageCount + }) + + const currentPageBridge = computed({ + get() { + return isAbsent(props.currentPage) ? innerCurrentPage.value : props.currentPage + }, + set(v) { + let newCurrentPage = v + if (v < 1) { + newCurrentPage = 1 + } else if (v > pageCountBridge.value) { + newCurrentPage = pageCountBridge.value + } + if (isAbsent(props.currentPage)) { + innerCurrentPage.value = newCurrentPage + } + if (hasCurrentPageListener) { + emit('update:current-page', newCurrentPage) + emit('current-change', newCurrentPage) + } + }, + }) function handleCurrentChange(val: number) { - internalCurrentPage.value = getValidCurrentPage(val) - userChangePageSize.value = true - emitChange() + currentPageBridge.value = val } - function handleSizesChange(val: number) { - userChangePageSize.value = true - internalPageSize.value = val - emit('update:pageSize', val) - emit('size-change', val) + function handleSizeChange(val: number) { + pageSizeBridge.value = val + const newPageCount = pageCountBridge.value + if (currentPageBridge.value > newPageCount) { + currentPageBridge.value = newPageCount + } } function prev() { if (props.disabled) return - const newVal = internalCurrentPage.value - 1 - internalCurrentPage.value = getValidCurrentPage(newVal) - emit('prev-click', internalCurrentPage.value) - emitChange() + currentPageBridge.value -= 1 + emit('prev-click', currentPageBridge.value) } function next() { if (props.disabled) return - const newVal = internalCurrentPage.value + 1 - internalCurrentPage.value = getValidCurrentPage(newVal) - emit('next-click', internalCurrentPage.value) - emitChange() + currentPageBridge.value += 1 + emit('next-click', currentPageBridge.value) } - function getValidCurrentPage(value: number | string) { - if (typeof value === 'string') { - value = parseInt(value, 10) - } - - let resetValue: number | undefined - - if (isNaN(value) || value < 1) { - resetValue = 1 - } else if (internalPageCount.value < value){ - resetValue = internalPageCount.value - } - - return resetValue ?? value - } - - watch(() => props.currentPage, val => { - internalCurrentPage.value = getValidCurrentPage(val) - lastEmittedPage.value = internalCurrentPage.value - }) - - watch(() => props.pageSize, val => { - internalPageSize.value = getValidPageSize(val) - }) - - watch( - () => internalPageCount.value, - val => { - const oldPage = internalCurrentPage.value - if (val > 0 && oldPage === 0) { - internalCurrentPage.value = 1 - } else if (oldPage > val) { - internalCurrentPage.value = val === 0 ? 1 : val - emitChange() - } - }, - ) - provide('pagination', { - pageCount: computed(() => props.pageCount), + pageCount: pageCountBridge, disabled: computed(() => props.disabled), - currentPage: computed(() => internalCurrentPage.value), + currentPage: currentPageBridge, changeEvent: handleCurrentChange, - handleSizesChange, + handleSizeChange, }) - return { - internalCurrentPage, - internalPageSize, - lastEmittedPage, - userChangePageSize, - internalPageCount, - - getValidCurrentPage, - emitChange, - handleCurrentChange, - - prev, - next, - } - }, - render() { - const layout = this.layout - - if (!layout) return null - if (this.hideOnSinglePage && this.internalPageCount <= 1) return null - - const rootNode = h('div', { - class: [ - 'el-pagination', - { - 'is-background': this.background, - 'el-pagination--small': this.small, - }, - ], - }) - const rootChildren = [] - const rightWrapperChildren = [] - const rightWrapperRoot = h('div', { class: 'el-pagination__rightwrapper' }, rightWrapperChildren) - const TEMPLATE_MAP = { - prev: h(Prev, { - disabled: this.disabled, - currentPage: this.internalCurrentPage, - prevText: this.prevText, - onClick: this.prev, - }), - jumper: h(Jumper), - pager: h(Pager, { - currentPage: this.internalCurrentPage, - pageCount: this.internalPageCount, - pagerCount: this.pagerCount, - onChange: this.handleCurrentChange, - disabled: this.disabled, - }), - next: h(Next, { - disabled: this.disabled, - currentPage: this.internalCurrentPage, - pageCount: this.internalPageCount, - nextText: this.nextText, - onClick: this.next, - }), - sizes: h(Sizes, { - pageSize: this.pageSize, - pageSizes: this.pageSizes, - popperClass: this.popperClass, - disabled: this.disabled, - }), - slot: this.$slots?.default?.() ?? null, - total: h(Total, { total: this.total }), - } - - const components = layout.split(',').map((item: string) => item.trim()) - - let haveRightWrapper = false - - components.forEach((c: keyof typeof TEMPLATE_MAP | '->') => { - if (c === '->') { - haveRightWrapper = true - return + return () => { + if (!assertValidUsage.value) { + warn(componentName, t('el.pagination.docRefer')) + return null } - if (!haveRightWrapper) { - rootChildren.push(TEMPLATE_MAP[c]) - } else { - rightWrapperChildren.push(TEMPLATE_MAP[c]) + if (!props.layout) return null + if (props.hideOnSinglePage && pageCountBridge.value <= 1) return null + const rootChildren = [] + const rightWrapperChildren = [] + const rightWrapperRoot = h('div', { class: 'el-pagination__rightwrapper' }, rightWrapperChildren) + const TEMPLATE_MAP = { + prev: h(Prev, { + disabled: props.disabled, + currentPage: currentPageBridge.value, + prevText: props.prevText, + onClick: prev, + }), + jumper: h(Jumper), + pager: h(Pager, { + currentPage: currentPageBridge.value, + pageCount: pageCountBridge.value, + pagerCount: props.pagerCount, + onChange: handleCurrentChange, + disabled: props.disabled, + }), + next: h(Next, { + disabled: props.disabled, + currentPage: currentPageBridge.value, + pageCount: pageCountBridge.value, + nextText: props.nextText, + onClick: next, + }), + sizes: h(Sizes, { + pageSize: pageSizeBridge.value, + pageSizes: props.pageSizes, + popperClass: props.popperClass, + disabled: props.disabled, + }), + slot: slots?.default?.() ?? null, + total: h(Total, { total: isAbsent(props.total) ? 0 : props.total }), } - }) - if (haveRightWrapper && rightWrapperChildren.length > 0) { - rootChildren.unshift(rightWrapperRoot) + const components = props.layout.split(',').map((item: string) => item.trim()) + + let haveRightWrapper = false + + components.forEach((c: keyof typeof TEMPLATE_MAP | '->') => { + if (c === '->') { + haveRightWrapper = true + return + } + if (!haveRightWrapper) { + rootChildren.push(TEMPLATE_MAP[c]) + } else { + rightWrapperChildren.push(TEMPLATE_MAP[c]) + } + }) + + if (haveRightWrapper && rightWrapperChildren.length > 0) { + rootChildren.unshift(rightWrapperRoot) + } + + return h('div', { + role: 'pagination', + 'aria-label': 'pagination', + class: [ + 'el-pagination', + { + 'is-background': props.background, + 'el-pagination--small': props.small, + }, + ], + }, rootChildren) } - - return h(rootNode, {}, rootChildren) }, }) diff --git a/packages/pagination/src/next.vue b/packages/pagination/src/next.vue index a2b7f2d61b..766263970d 100644 --- a/packages/pagination/src/next.vue +++ b/packages/pagination/src/next.vue @@ -3,6 +3,7 @@ type="button" class="btn-next" :disabled="internalDisabled" + :aria-disabled="internalDisabled" @click.self.prevent > {{ nextText }} diff --git a/packages/pagination/src/pager.vue b/packages/pagination/src/pager.vue index 0b2e559ba4..84c2f5e5d2 100644 --- a/packages/pagination/src/pager.vue +++ b/packages/pagination/src/pager.vue @@ -1,9 +1,11 @@