feat(datatable) : pagination compacte avec saut de page (Page [n] / N)

This commit is contained in:
2026-06-09 15:06:10 +02:00
parent b2c6f33e38
commit f797c1c8a0
+49 -54
View File
@@ -81,43 +81,39 @@
<nav aria-label="Pagination" class="flex items-center gap-1" data-test="pagination-nav"> <nav aria-label="Pagination" class="flex items-center gap-1" data-test="pagination-nav">
<MalioButton <MalioButton
variant="tertiary" variant="tertiary"
label="Prev" label="Préc."
:disabled="page <= 1" :disabled="page <= 1"
button-class="h-[30px] w-auto min-w-0 px-3 text-sm" button-class="h-[30px] w-auto min-w-0 px-3 text-sm"
aria-label="Page précédente" aria-label="Page précédente"
data-test="prev-button" data-test="prev-button"
@click="goToPage(page - 1)" @click="changePage(page - 1)"
/> />
<template v-for="(p, idx) in visiblePages" :key="idx"> <span class="flex items-center gap-2 text-sm">
<span <label :for="pageInputId" class="text-m-muted">Page</label>
v-if="p === '...'" <input
class="px-1 text-sm text-m-muted" :id="pageInputId"
aria-hidden="true" v-model="pageInput"
></span> type="text"
<button inputmode="numeric"
v-else aria-label="Aller à la page"
type="button" data-test="page-input"
class="inline-flex h-[30px] min-w-[2.5rem] items-center justify-center rounded px-2 text-sm transition-colors" class="h-[30px] w-[58px] rounded-malio border border-m-border text-center text-sm text-m-text outline-none focus:border-m-primary"
:class="p === page @input="onPageInput"
? 'bg-m-btn-primary text-white font-semibold' @keydown.enter="commitPageInput"
: 'text-m-text hover:bg-m-bg'" @blur="commitPageInput"
:aria-current="p === page ? 'page' : undefined"
:data-test="`page-${p}`"
@click="goToPage(p)"
> >
{{ p }} <span class="text-m-muted">/ <span data-test="total-pages">{{ totalPages }}</span></span>
</button> </span>
</template>
<MalioButton <MalioButton
variant="tertiary" variant="tertiary"
label="Next" label="Suiv."
:disabled="page >= totalPages" :disabled="page >= totalPages"
button-class="h-[30px] w-auto min-w-0 px-3 text-sm" button-class="h-[30px] w-auto min-w-0 px-3 text-sm"
aria-label="Page suivante" aria-label="Page suivante"
data-test="next-button" data-test="next-button"
@click="goToPage(page + 1)" @click="changePage(page + 1)"
/> />
</nav> </nav>
</div> </div>
@@ -125,7 +121,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, useAttrs, useId } from 'vue' import { computed, ref, watch, onBeforeUnmount, useAttrs, useId } from 'vue'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import MalioSelect from '../select/Select.vue' import MalioSelect from '../select/Select.vue'
import MalioButton from '../button/Button.vue' import MalioButton from '../button/Button.vue'
@@ -173,6 +169,15 @@ const componentId = computed(() => props.id || `malio-datatable-${generatedId}`)
const totalPages = computed(() => Math.max(1, Math.ceil(props.totalItems / props.perPage))) const totalPages = computed(() => Math.max(1, Math.ceil(props.totalItems / props.perPage)))
const PAGE_JUMP_DEBOUNCE = 400
let debounceTimer: ReturnType<typeof setTimeout> | null = null
const pageInputId = computed(() => `${componentId.value}-page-input`)
const pageInput = ref(String(props.page))
watch(() => props.page, (p) => { pageInput.value = String(p) })
onBeforeUnmount(() => { if (debounceTimer) clearTimeout(debounceTimer) })
const perPageSelectOptions = computed(() => const perPageSelectOptions = computed(() =>
props.perPageOptions.map(n => ({ label: String(n), value: n })) props.perPageOptions.map(n => ({ label: String(n), value: n }))
) )
@@ -184,42 +189,32 @@ function onPerPageChange(value: string | number | null) {
} }
} }
function goToPage(page: number) { function changePage(page: number) {
if (page >= 1 && page <= totalPages.value) { if (page >= 1 && page <= totalPages.value && page !== props.page) {
emit('update:page', page) emit('update:page', page)
} }
} }
const visiblePages = computed(() => { function onPageInput() {
const total = totalPages.value pageInput.value = pageInput.value.replace(/[^0-9]/g, '')
const current = props.page if (debounceTimer) clearTimeout(debounceTimer)
if (pageInput.value === '') return
if (total <= 5) { const n = Number(pageInput.value)
return Array.from({ length: total }, (_, i) => i + 1) if (n >= 1 && n <= totalPages.value) {
debounceTimer = setTimeout(() => changePage(n), PAGE_JUMP_DEBOUNCE)
}
} }
const pages: (number | '...')[] = [] function commitPageInput() {
pages.push(1) if (debounceTimer) clearTimeout(debounceTimer)
const raw = pageInput.value.trim()
if (current > 3) { const n = Number(raw)
pages.push('...') if (raw === '' || n === 0 || Number.isNaN(n)) {
pageInput.value = String(props.page)
return
} }
const clamped = Math.min(Math.max(1, Math.round(n)), totalPages.value)
const start = Math.max(2, current - 1) changePage(clamped)
const end = Math.min(total - 1, current + 1) pageInput.value = String(clamped)
for (let i = start; i <= end; i++) {
pages.push(i)
} }
if (current < total - 2) {
pages.push('...')
}
if (total > 1) {
pages.push(total)
}
return pages
})
</script> </script>