-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: APPS-1974_Add numbered page buttons to SectionPagination (#647)
* feat: pagination part 1 * feat: finish truncation logic * style: FTVA pagination styles * chore: documentation * fix: ts errors * chore: run linter * fix: prevent prev/ next button appearing in legacy * chore: cleanup * chore: refactor to computed methods --------- Co-authored-by: Jess Divers <[email protected]> Co-authored-by: JenDiamond <[email protected]>
- Loading branch information
1 parent
a41c820
commit 9210fce
Showing
4 changed files
with
314 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,116 +1,196 @@ | ||
<script lang="ts" setup> | ||
import { computed } from 'vue' | ||
// SVGs | ||
import { computed, nextTick, onMounted, ref, watch } from 'vue' | ||
import type { Ref } from 'vue' | ||
import { useWindowSize } from '@vueuse/core' | ||
import SvgIconArrowRight from 'ucla-library-design-tokens/assets/svgs/icon-arrow-right.svg' | ||
import { useTheme } from '@/composables/useTheme' | ||
// COMPONENTS | ||
import SmartLink from '@/lib-components/SmartLink.vue' | ||
// PROPS & DATA | ||
const { nextTo, previousTo } = defineProps({ | ||
const { nextTo, previousTo, pages, initialCurrentPage } = defineProps({ | ||
nextTo: { | ||
type: String, | ||
default: '', | ||
required: false, | ||
}, | ||
previousTo: { | ||
type: String, | ||
default: '', | ||
required: false, | ||
}, | ||
pages: { | ||
type: Number, | ||
required: false, | ||
}, | ||
initialCurrentPage: { | ||
type: Number, | ||
required: false, | ||
}, | ||
}) | ||
const emit = defineEmits(['changePage']) // let parent component know when page changes | ||
const theme = useTheme() | ||
const maxPages = ref(10) // default # of buttons that will fit in container, gets recalculated onMount & resize | ||
const leftPages = ref([33]) // an array of numbers representing the page buttons that will appear ( we start with a single '33' so we can measure the width of a button to calc maxPages) | ||
const currPage = ref(1) // current page, defaults to 1 | ||
const pageButtons: Ref<HTMLElement | null> = ref(null) | ||
// METHODS | ||
function handlePageChange(item: number) { | ||
if (initialCurrentPage && pages) { | ||
if (currPage.value !== item) { | ||
currPage.value = item | ||
generateLeftPages() | ||
emit('changePage', item) | ||
} | ||
} | ||
} | ||
function generateLeftPages() { | ||
if (pages && maxPages) { | ||
let start = 1 | ||
// stop at either maxPages or total pages, whichever is lesser | ||
let stop = Math.min(maxPages.value, pages) | ||
// if current page is greater than than maxPages, | ||
// put current page in middle of range of generated page number buttons | ||
if (currPage.value > maxPages.value) { | ||
let newMaxPages = maxPages.value - 4 // subtract 4 for '...' first/last number buttons | ||
start = Math.max(1, currPage.value - Math.floor(newMaxPages / 2)) | ||
stop = start + newMaxPages | ||
// if current page is very near the last page, | ||
// we need to remove the truncation button near the end | ||
if (stop > pages) { | ||
newMaxPages = newMaxPages + 1 // add 1 back for missing '...' button | ||
if (currPage.value === pages) | ||
newMaxPages = newMaxPages + 1 // add another 1 back because 'next' button is hidden | ||
stop = pages | ||
start = Math.max(1, stop - newMaxPages) | ||
} | ||
} | ||
// if we're on first page | ||
if (currPage.value === 1) { | ||
// add 1 more button to the end because 'prev' button is hidden, unless thay would exceed total pages | ||
stop = Math.min(stop + 2, pages) | ||
} | ||
leftPages.value = [] | ||
for (let i = start; i <= stop; i++) | ||
leftPages.value.push(i) | ||
} | ||
} | ||
function setPaginationMaxPages(width: number) { | ||
// fail gracefully with 10 as a the default | ||
if (!initialCurrentPage || !pages) | ||
return 10 | ||
// get width of buttons | ||
const button = document.getElementsByClassName('pButton')[0] | ||
const buttonWidth = Math.ceil(button.getBoundingClientRect().width) | ||
const buttonMargin = getComputedStyle(button).marginRight | ||
const itemWidth = Math.ceil(buttonWidth + (Number.parseInt(buttonMargin) * 2) + 1) // we add 1 to give us a little leeway | ||
const prevButtonWidth = Math.ceil(document.getElementsByClassName('previous')[0].getBoundingClientRect().width + 10) | ||
const nextButtonWidth = Math.ceil(document.getElementsByClassName('next')[0].getBoundingClientRect().width + 10) | ||
// calc # of buttons that can fit | ||
// take width minus the width of: 2 page buttons (last button and '...'), 2 prev/next buttons | ||
const MaxButtons = Math.max(0, Math.floor(+((width - (prevButtonWidth + nextButtonWidth + (itemWidth * 2))) / itemWidth).toFixed(2))) | ||
return MaxButtons | ||
} | ||
// COMPUTED | ||
const classes = computed(() => { | ||
if (previousTo === '') | ||
return ['section-pagination', 'first-page'] | ||
return ['section-pagination', theme?.value || '', previousTo === '' ? 'first-page' : '', nextTo === '' ? 'last-page' : ''] | ||
}) | ||
const parsedPrevTo = computed(() => { | ||
return currPage.value - 1 | ||
}) | ||
const parsedNextTo = computed(() => { | ||
return currPage.value + 1 | ||
}) | ||
const isNotFirstPage = computed(() => { | ||
return (initialCurrentPage && pages) && currPage?.value !== 1 | ||
}) | ||
const isNotLastPage = computed(() => { | ||
return (initialCurrentPage && pages) && currPage?.value !== pages | ||
}) | ||
if (nextTo === '') | ||
return ['section-pagination', 'last-page'] | ||
else | ||
return ['section-pagination'] | ||
onMounted(() => { | ||
// legacy implementation does not require any onMounted logic | ||
if (!initialCurrentPage || !pages) | ||
return | ||
currPage.value = initialCurrentPage | ||
const { width } = useWindowSize() | ||
// wait for next tick to ensure children are rendered and width is correct | ||
nextTick(() => { | ||
// watch for width changes and update # of buttons that will fit | ||
watch([width], () => { | ||
const paginationWidth = pageButtons.value!.clientWidth | ||
maxPages.value = setPaginationMaxPages(paginationWidth) as number | ||
generateLeftPages() // then generate buttons representing pages | ||
}, { immediate: true }) | ||
}) | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div :class="classes"> | ||
<div ref="pageButtons" :class="classes" role="navigation" aria-label="page list navigation"> | ||
<!-- if legacy attribute previousTo is supplied, use that for Prev button instead of handlePageChange --> | ||
<SmartLink v-if="previousTo" :to="previousTo" class="previous"> | ||
<SvgIconArrowRight class="previous-svg" /> | ||
<div class="underline-hover"> | ||
Previous | ||
</div> | ||
</SmartLink> | ||
<SmartLink v-else-if="isNotFirstPage" class="previous" @click="handlePageChange(parsedPrevTo)"> | ||
<SvgIconArrowRight class="previous-svg" /> | ||
<div class="underline-hover"> | ||
Previous | ||
</div> | ||
</SmartLink> | ||
<div v-if="initialCurrentPage && pages" class="pagination-numbers-container"> | ||
<div class="pagination-numbers"> | ||
<span v-if="currPage > maxPages" class="page-list-first"><button | ||
:class="`pButton${1 === currPage ? ' ' + 'pButton-selected' : ''}`" | ||
@click="handlePageChange(1)" | ||
>{{ 1 }}</button> | ||
</span> | ||
<span v-if="currPage > maxPages" class="page-list-truncate">...</span> | ||
<button | ||
v-for="item in leftPages" | ||
:key="item" | ||
:class="`pButton${item === currPage ? ' ' + 'pButton-selected' : ''}`" | ||
@click="handlePageChange(item)" | ||
> | ||
{{ item }} | ||
</button> | ||
<span v-if="leftPages.length < pages && leftPages.indexOf(pages) === -1" class="page-list-truncate">...</span> | ||
<span v-if="leftPages.length < pages && leftPages.indexOf(pages) === -1" class="page-list-right"><button | ||
:class="`pButton${pages === currPage ? ' ' + 'pButton-selected' : ''}`" | ||
@click="handlePageChange(pages)" | ||
>{{ pages }}</button></span> | ||
</div> | ||
</div> | ||
<!-- if legacy attribute nextTo is supplied, use that for Next button instead of handlePageChange --> | ||
<SmartLink v-if="nextTo" :to="nextTo" class="next"> | ||
<div class="underline-hover"> | ||
Next | ||
</div> | ||
<SvgIconArrowRight class="next-svg" /> | ||
</SmartLink> | ||
<SmartLink v-else-if="isNotLastPage" class="next" @click="handlePageChange(parsedNextTo)"> | ||
<div class="underline-hover"> | ||
Next | ||
</div> | ||
<SvgIconArrowRight class="next-svg" /> | ||
</SmartLink> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" scoped> | ||
.section-pagination { | ||
max-width: 990px; | ||
font-family: var(--font-secondary); | ||
font-size: 18px; | ||
padding-left: 13px; | ||
display: flex; | ||
flex-direction: row; | ||
flex-wrap: nowrap; | ||
justify-content: space-between; | ||
align-content: center; | ||
align-items: center; | ||
.previous { | ||
display: flex; | ||
align-items: center; | ||
} | ||
.next { | ||
display: flex; | ||
align-items: center; | ||
} | ||
.previous-svg { | ||
transform: scaleX(-1); | ||
margin-right: 5px; | ||
} | ||
.next-svg { | ||
margin-left: 5px; | ||
} | ||
&.first-page { | ||
display: flex; | ||
flex-direction: row; | ||
flex-wrap: nowrap; | ||
justify-content: flex-end; | ||
align-content: stretch; | ||
align-items: center; | ||
} | ||
&.last-page { | ||
display: flex; | ||
flex-direction: row; | ||
flex-wrap: nowrap; | ||
justify-content: flex-start; | ||
align-content: stretch; | ||
align-items: center; | ||
} | ||
// Hover states | ||
@media #{$has-hover} { | ||
.previous:hover, | ||
.previous:active { | ||
color: var(--color-primary-blue-03); | ||
} | ||
.next:hover, | ||
.next:active { | ||
color: var(--color-primary-blue-03); | ||
} | ||
} | ||
} | ||
@import "@/styles/default/_section-pagination.scss"; | ||
@import "@/styles/ftva/_section-pagination.scss"; | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
9210fce
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Published on https://ucla-library-storybook.netlify.app as production
🚀 Deployed on https://672e584aab98e40cb05f85b0--ucla-library-storybook.netlify.app