From 26562ed11deef4b99fb88e07d7b0a59fc4f13be1 Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 3 Apr 2026 20:58:03 +0200 Subject: [PATCH 1/7] feat(VPagination): add show first/last page props --- .../VDataTable/VDataTableFooter.tsx | 14 +++++++++ .../components/VPagination/VPagination.tsx | 29 +++++++++++++++---- .../__tests__/VPagination.spec.browser.tsx | 27 +++++++++++++++++ packages/vuetify/vitest.config.ts | 6 ++-- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx b/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx index b67d27ec0c4..507cd24785f 100644 --- a/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx +++ b/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx @@ -70,6 +70,18 @@ export const makeVDataTableFooterProps = propsFactory({ ]), }, showCurrentPage: Boolean, + showFirstPage: { + type: Boolean, + default: false, + }, + showLastPage: { + type: Boolean, + default: false, + }, + showFirstLastPage: { + type: Boolean, + default: true, + }, }, 'VDataTableFooter') export const VDataTableFooter = genericComponent<{ prepend: never }>()({ @@ -137,6 +149,8 @@ export const VDataTableFooter = genericComponent<{ prepend: never }>()({ nextAriaLabel={ props.nextPageLabel } previousAriaLabel={ props.prevPageLabel } rounded + showFirstPage + showLastPage showFirstLastPage totalVisible={ props.showCurrentPage ? 1 : 0 } variant="plain" diff --git a/packages/vuetify/src/components/VPagination/VPagination.tsx b/packages/vuetify/src/components/VPagination/VPagination.tsx index 5ca07fbfc5a..af4f8ab0572 100644 --- a/packages/vuetify/src/components/VPagination/VPagination.tsx +++ b/packages/vuetify/src/components/VPagination/VPagination.tsx @@ -117,7 +117,18 @@ export const makeVPaginationProps = propsFactory({ type: String, default: '...', }, - showFirstLastPage: Boolean, + showFirstPage: { + type: Boolean, + default: false, + }, + showLastPage: { + type: Boolean, + default: false, + }, + showFirstLastPage: { + type: Boolean, + default: true, + }, ...makeBorderProps(), ...makeComponentProps(), @@ -278,7 +289,7 @@ export const VPagination = genericComponent()({ const nextDisabled = !!props.disabled || page.value >= start.value + length.value - 1 return { - first: props.showFirstLastPage ? { + first: (props.showFirstPage || props.showFirstLastPage) ? { icon: isRtl.value ? props.lastIcon : props.firstIcon, onClick: (e: Event) => setValue(e, start.value, 'first'), disabled: prevDisabled, @@ -299,7 +310,7 @@ export const VPagination = genericComponent()({ 'aria-label': t(props.nextAriaLabel), 'aria-disabled': nextDisabled, }, - last: props.showFirstLastPage ? { + last: (props.showLastPage || props.showFirstLastPage) ? { icon: isRtl.value ? props.firstIcon : props.lastIcon, onClick: (e: Event) => setValue(e, start.value + length.value - 1, 'last'), disabled: nextDisabled, @@ -339,8 +350,13 @@ export const VPagination = genericComponent()({ data-test="v-pagination-root" >
    - { props.showFirstLastPage && ( -
  • + { (props.showFirstPage || props.showFirstLastPage) && ( +
  • { slots.first ? slots.first(controls.value.first!) : ( )} @@ -380,11 +396,12 @@ export const VPagination = genericComponent()({ )}
  • - { props.showFirstLastPage && ( + { (props.showLastPage || props.showFirstLastPage) && (
  • { slots.last ? slots.last(controls.value.last!) : ( diff --git a/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx b/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx index 09d176d2be5..d5b785613bf 100644 --- a/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx +++ b/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx @@ -23,6 +23,33 @@ describe('VPagination', () => { expect(screen.getAllByCSS('.v-pagination__item')).toHaveLength(3) }) + it('should render with first and last page buttons', () => { + render(() => ( + + )) + + expect(page.getByTestId('v-pagination-first')).toBeInTheDocument() + expect(page.getByTestId('v-pagination-last')).toBeInTheDocument() + }) + + it('should render without first and last page buttons', () => { + render(() => ( + + )) + + expect(page.getByTestId('v-pagination-first')).not.toBeInTheDocument() + expect(page.getByTestId('v-pagination-last')).not.toBeInTheDocument() + }) + + it('should render without last page button', () => { + render(() => ( + + )) + + expect(page.getByTestId('v-pagination-first')).toBeInTheDocument() + expect(page.getByTestId('v-pagination-last')).not.toBeInTheDocument() + }) + it('should react to mouse navigation', async () => { render(() => ( diff --git a/packages/vuetify/vitest.config.ts b/packages/vuetify/vitest.config.ts index 7a151d362b6..906e00124fe 100644 --- a/packages/vuetify/vitest.config.ts +++ b/packages/vuetify/vitest.config.ts @@ -51,7 +51,7 @@ export default defineConfig(configEnv => { 'process.env.TEST_TDD_ONLY': process.env.TEST_TDD_ONLY, }, test: { - watch: false, + watch: true, slowTestThreshold: Infinity, setupFiles: ['../test/setup/to-have-been-warned.ts'], reporters: process.env.GITHUB_ACTIONS @@ -104,8 +104,8 @@ export default defineConfig(configEnv => { ].filter(v => v != null), }, }), - ui: false, - headless: !process.env.TEST_BAIL, + ui: true, + headless: false, //! process.env.TEST_BAIL, screenshotDirectory: '../test/__screenshots__', commands, instances: [{ From f6fcbf93e6cd429c633fcc4b6437f5f2b142b33b Mon Sep 17 00:00:00 2001 From: userquin Date: Fri, 3 Apr 2026 21:06:08 +0200 Subject: [PATCH 2/7] chore: revert local vitest config changes --- packages/vuetify/vitest.config.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vuetify/vitest.config.ts b/packages/vuetify/vitest.config.ts index 906e00124fe..7a151d362b6 100644 --- a/packages/vuetify/vitest.config.ts +++ b/packages/vuetify/vitest.config.ts @@ -51,7 +51,7 @@ export default defineConfig(configEnv => { 'process.env.TEST_TDD_ONLY': process.env.TEST_TDD_ONLY, }, test: { - watch: true, + watch: false, slowTestThreshold: Infinity, setupFiles: ['../test/setup/to-have-been-warned.ts'], reporters: process.env.GITHUB_ACTIONS @@ -104,8 +104,8 @@ export default defineConfig(configEnv => { ].filter(v => v != null), }, }), - ui: true, - headless: false, //! process.env.TEST_BAIL, + ui: false, + headless: !process.env.TEST_BAIL, screenshotDirectory: '../test/__screenshots__', commands, instances: [{ From fd64011515c1f6839934c324cf1487044d34f9cd Mon Sep 17 00:00:00 2001 From: J-Sek Date: Fri, 3 Apr 2026 22:18:05 +0200 Subject: [PATCH 3/7] refactor: extend existing prop for better DX --- .../VDataTable/VDataTableFooter.tsx | 19 ++++------------ .../components/VPagination/VPagination.tsx | 22 ++++++------------- .../__tests__/VPagination.spec.browser.tsx | 11 +--------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx b/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx index 507cd24785f..402e29a4acb 100644 --- a/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx +++ b/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx @@ -12,10 +12,11 @@ import { useLocale } from '@/composables/locale' // Utilities import { computed } from 'vue' -import { genericComponent, omit, propsFactory, useRender } from '@/util' +import { genericComponent, omit, pick, propsFactory, useRender } from '@/util' // Types import type { PropType } from 'vue' +import { makeVPaginationProps } from '../VPagination/VPagination' export const makeVDataTableFooterProps = propsFactory({ color: String, @@ -70,18 +71,8 @@ export const makeVDataTableFooterProps = propsFactory({ ]), }, showCurrentPage: Boolean, - showFirstPage: { - type: Boolean, - default: false, - }, - showLastPage: { - type: Boolean, - default: false, - }, - showFirstLastPage: { - type: Boolean, - default: true, - }, + + ...pick(makeVPaginationProps(), ['showFirstLastPage']), }, 'VDataTableFooter') export const VDataTableFooter = genericComponent<{ prepend: never }>()({ @@ -149,8 +140,6 @@ export const VDataTableFooter = genericComponent<{ prepend: never }>()({ nextAriaLabel={ props.nextPageLabel } previousAriaLabel={ props.prevPageLabel } rounded - showFirstPage - showLastPage showFirstLastPage totalVisible={ props.showCurrentPage ? 1 : 0 } variant="plain" diff --git a/packages/vuetify/src/components/VPagination/VPagination.tsx b/packages/vuetify/src/components/VPagination/VPagination.tsx index af4f8ab0572..0527b2ec986 100644 --- a/packages/vuetify/src/components/VPagination/VPagination.tsx +++ b/packages/vuetify/src/components/VPagination/VPagination.tsx @@ -27,7 +27,7 @@ import { computed, nextTick, shallowRef, toRef } from 'vue' import { createRange, genericComponent, keyValues, propsFactory, useRender } from '@/util' // Types -import type { ComponentPublicInstance } from 'vue' +import type { ComponentPublicInstance, PropType } from 'vue' type ItemSlot = { isActive: boolean @@ -117,17 +117,9 @@ export const makeVPaginationProps = propsFactory({ type: String, default: '...', }, - showFirstPage: { - type: Boolean, - default: false, - }, - showLastPage: { - type: Boolean, - default: false, - }, showFirstLastPage: { - type: Boolean, - default: true, + type: [Boolean, String] as PropType, + default: false, }, ...makeBorderProps(), @@ -289,7 +281,7 @@ export const VPagination = genericComponent()({ const nextDisabled = !!props.disabled || page.value >= start.value + length.value - 1 return { - first: (props.showFirstPage || props.showFirstLastPage) ? { + first: [true, 'only-first'].includes(props.showFirstLastPage) ? { icon: isRtl.value ? props.lastIcon : props.firstIcon, onClick: (e: Event) => setValue(e, start.value, 'first'), disabled: prevDisabled, @@ -310,7 +302,7 @@ export const VPagination = genericComponent()({ 'aria-label': t(props.nextAriaLabel), 'aria-disabled': nextDisabled, }, - last: (props.showLastPage || props.showFirstLastPage) ? { + last: props.showFirstLastPage === true ? { icon: isRtl.value ? props.firstIcon : props.lastIcon, onClick: (e: Event) => setValue(e, start.value + length.value - 1, 'last'), disabled: nextDisabled, @@ -350,7 +342,7 @@ export const VPagination = genericComponent()({ data-test="v-pagination-root" >
      - { (props.showFirstPage || props.showFirstLastPage) && ( + {[true, 'only-first'].includes(props.showFirstLastPage) && (
    • ()({ )}
    • - { (props.showLastPage || props.showFirstLastPage) && ( + { props.showFirstLastPage === true && (
    • { expect(screen.getAllByCSS('.v-pagination__item')).toHaveLength(3) }) - it('should render with first and last page buttons', () => { - render(() => ( - - )) - - expect(page.getByTestId('v-pagination-first')).toBeInTheDocument() - expect(page.getByTestId('v-pagination-last')).toBeInTheDocument() - }) - it('should render without first and last page buttons', () => { render(() => ( @@ -43,7 +34,7 @@ describe('VPagination', () => { it('should render without last page button', () => { render(() => ( - + )) expect(page.getByTestId('v-pagination-first')).toBeInTheDocument() From a15cb7b2a1bf43d3e5e030b9410337896cbb0c25 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Fri, 3 Apr 2026 23:58:35 +0200 Subject: [PATCH 4/7] fix: restore first/last in VDataTable --- .../vuetify/src/components/VDataTable/VDataTableFooter.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx b/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx index 402e29a4acb..f2edb09ec94 100644 --- a/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx +++ b/packages/vuetify/src/components/VDataTable/VDataTableFooter.tsx @@ -72,7 +72,9 @@ export const makeVDataTableFooterProps = propsFactory({ }, showCurrentPage: Boolean, - ...pick(makeVPaginationProps(), ['showFirstLastPage']), + ...pick(makeVPaginationProps({ + showFirstLastPage: true, + }), ['showFirstLastPage']), }, 'VDataTableFooter') export const VDataTableFooter = genericComponent<{ prepend: never }>()({ From d0449929f547b07659232fbb7e21c19d8733aa02 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Mon, 6 Apr 2026 16:04:24 +0200 Subject: [PATCH 5/7] chore: revert unrelated changes --- .../src/components/VPagination/VPagination.tsx | 14 ++------------ .../__tests__/VPagination.spec.browser.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/packages/vuetify/src/components/VPagination/VPagination.tsx b/packages/vuetify/src/components/VPagination/VPagination.tsx index 0527b2ec986..0ccb39885e5 100644 --- a/packages/vuetify/src/components/VPagination/VPagination.tsx +++ b/packages/vuetify/src/components/VPagination/VPagination.tsx @@ -343,12 +343,7 @@ export const VPagination = genericComponent()({ >
        {[true, 'only-first'].includes(props.showFirstLastPage) && ( -
      • +
      • { slots.first ? slots.first(controls.value.first!) : ( )} @@ -389,12 +384,7 @@ export const VPagination = genericComponent()({
      • { props.showFirstLastPage === true && ( -
      • +
      • { slots.last ? slots.last(controls.value.last!) : ( )} diff --git a/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx b/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx index af0f2455e1a..f2f4cf24cba 100644 --- a/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx +++ b/packages/vuetify/src/components/VPagination/__tests__/VPagination.spec.browser.tsx @@ -28,8 +28,8 @@ describe('VPagination', () => { )) - expect(page.getByTestId('v-pagination-first')).not.toBeInTheDocument() - expect(page.getByTestId('v-pagination-last')).not.toBeInTheDocument() + expect(screen.queryByCSS('[data-test="v-pagination-first"]')).not.toBeInTheDocument() + expect(screen.queryByCSS('[data-test="v-pagination-last"]')).not.toBeInTheDocument() }) it('should render without last page button', () => { @@ -37,8 +37,8 @@ describe('VPagination', () => { )) - expect(page.getByTestId('v-pagination-first')).toBeInTheDocument() - expect(page.getByTestId('v-pagination-last')).not.toBeInTheDocument() + expect(screen.queryByCSS('[data-test="v-pagination-first"]')).toBeInTheDocument() + expect(screen.queryByCSS('[data-test="v-pagination-last"]')).not.toBeInTheDocument() }) it('should react to mouse navigation', async () => { From 1a40a2ad4c2e95efe5f0f2d8698dc4a731fe544a Mon Sep 17 00:00:00 2001 From: J-Sek Date: Mon, 6 Apr 2026 16:04:36 +0200 Subject: [PATCH 6/7] docs: update prop description --- packages/api-generator/src/locale/en/VPagination.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api-generator/src/locale/en/VPagination.json b/packages/api-generator/src/locale/en/VPagination.json index 2c222a10404..dc5a54b0caa 100644 --- a/packages/api-generator/src/locale/en/VPagination.json +++ b/packages/api-generator/src/locale/en/VPagination.json @@ -14,7 +14,7 @@ "pageAriaLabel": "Label for each page button.", "prevIcon": "The icon to use for the prev button.", "previousAriaLabel": "Label for the previous button.", - "showFirstLastPage": "Show buttons for going to first and last page.", + "showFirstLastPage": "Show buttons for going to first and last page. Since v4.1.0 it accepts `'only-first'`, to only the first page button.", "start": "Specify the starting page.", "totalVisible": "Specify the total visible pagination numbers." }, From 0473be8d0c7709a5ecf249dfede9793dfeac3fa5 Mon Sep 17 00:00:00 2001 From: J-Sek Date: Mon, 6 Apr 2026 16:14:13 +0200 Subject: [PATCH 7/7] docs: prop badges --- packages/docs/src/data/new-in.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/docs/src/data/new-in.json b/packages/docs/src/data/new-in.json index b539df23b0f..1a2812e97cd 100644 --- a/packages/docs/src/data/new-in.json +++ b/packages/docs/src/data/new-in.json @@ -133,6 +133,7 @@ "headerProps": "3.5.0", "initialSortOrder": "3.11.0", "pageBy": "3.12.0", + "showFirstLastPage": "4.1.0", "sortIcon": "3.12.0" }, "slots": { @@ -147,6 +148,7 @@ "groupExpandIcon": "3.10.0", "initialSortOrder": "3.11.0", "pageBy": "3.12.0", + "showFirstLastPage": "4.1.0", "sortIcon": "3.12.0" }, "slots": {