-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat(pagination): completed pagination component * chore: updated version * chore(dev-deps): updated dependencies - @types/requestidlecallback - @typescript-eslint/eslint-plugin - @typescript-eslint/parser - @vitejs/plugin-vue - autoprefixer - lint-staged - sass - ts-node - vite * test(pagination): added tests * style: spacing Add spacing between methods * test(pagination): updated snapshots * chore: added todo * style(pagination): added no selecting to the `...` characters * style(pagination): set min-width for page buttons
- Loading branch information
Showing
12 changed files
with
749 additions
and
235 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
Large diffs are not rendered by default.
Oops, something went wrong.
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,6 +1,6 @@ | ||
{ | ||
"name": "@karnama/vueish", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"files": [ | ||
"dist", | ||
"types" | ||
|
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
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 |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<template> | ||
<div class="mb-6 flex flex-col text-color"> | ||
<UICheckbox v-model="disabled" | ||
name="disabled" | ||
label="Disabled" | ||
class="mb-2" /> | ||
<label> | ||
Page count ({{ length }}) | ||
<input v-model="length" | ||
type="range" | ||
min="1" | ||
max="100" | ||
name="page-count"> | ||
</label> | ||
|
||
<label> | ||
Visible Buttons ({{ visible }}) | ||
<input v-model="visible" | ||
type="range" | ||
min="1" | ||
:max="length" | ||
step="2" | ||
name="visible-buttons"> | ||
</label> | ||
</div> | ||
|
||
<UIPagination v-model="page" | ||
:length="length" | ||
:disabled="disabled" | ||
:visible-count="visible" /> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import { defineComponent, ref } from 'vue'; | ||
export default defineComponent({ | ||
name: 'Pagination', | ||
setup() { | ||
const disabled = ref(false); | ||
const page = ref(5); | ||
const length = ref(10); | ||
const visible = ref(3); | ||
return { | ||
disabled, | ||
page, | ||
length, | ||
visible | ||
}; | ||
} | ||
}); | ||
</script> |
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 |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { mount } from '@vue/test-utils'; | ||
import UIPagination from './UIPagination.vue'; | ||
import { ref } from 'vue'; | ||
|
||
const commonProps = { | ||
modelValue: 5, | ||
length: 10, | ||
visibleCount: 3 | ||
} as const; | ||
|
||
describe('UIPagination', () => { | ||
it('should render correctly', () => { | ||
const wrapper = mount(UIPagination, { | ||
props: commonProps | ||
}); | ||
|
||
expect(wrapper.element).toMatchSnapshot(); | ||
}); | ||
|
||
it('should display the correct number of buttons', async () => { | ||
const wrapper = mount(UIPagination, { | ||
props: commonProps | ||
}); | ||
|
||
expect(wrapper.findAll('button')).toHaveLength( | ||
// previous and next buttons | ||
2 | ||
// the buttons | ||
+ commonProps.visibleCount | ||
// the wrapping end buttons | ||
+ 2 | ||
); | ||
|
||
await wrapper.setProps({ visibleCount: 5 }); | ||
expect(wrapper.findAll('button')).toHaveLength( | ||
// previous and next buttons | ||
2 | ||
// the buttons | ||
+ 5 | ||
// the wrapping end buttons | ||
+ 2 | ||
); | ||
}); | ||
|
||
it('should navigate to the previous page on previous button', async () => { | ||
const wrapper = mount(UIPagination, { | ||
props: commonProps | ||
}); | ||
|
||
const previousPageBtn = wrapper.find('#previous-page-button'); | ||
await previousPageBtn.trigger('click'); | ||
|
||
expect(wrapper.lastEventValue()).toStrictEqual([4]); | ||
await previousPageBtn.trigger('click'); | ||
await previousPageBtn.trigger('click'); | ||
await previousPageBtn.trigger('click'); | ||
expect(wrapper.lastEventValue()).toStrictEqual([1]); | ||
|
||
await previousPageBtn.trigger('click'); | ||
// it is now disabled so: | ||
expect(wrapper.lastEventValue()).toStrictEqual([1]); | ||
expect(wrapper.emitted('update:modelValue')).toHaveLength(4); | ||
}); | ||
|
||
it('should navigate to the next page on next button', async () => { | ||
const wrapper = mount(UIPagination, { | ||
props: commonProps | ||
}); | ||
|
||
const nextPageBtn = wrapper.find('#next-page-button'); | ||
await nextPageBtn.trigger('click'); | ||
|
||
expect(wrapper.lastEventValue()).toStrictEqual([6]); | ||
await nextPageBtn.trigger('click'); | ||
await nextPageBtn.trigger('click'); | ||
await nextPageBtn.trigger('click'); | ||
await nextPageBtn.trigger('click'); | ||
expect(wrapper.lastEventValue()).toStrictEqual([10]); | ||
|
||
await nextPageBtn.trigger('click'); | ||
// it is now disabled so: | ||
expect(wrapper.lastEventValue()).toStrictEqual([10]); | ||
expect(wrapper.emitted('update:modelValue')).toHaveLength(5); | ||
}); | ||
|
||
it('should handle model-binding correctly', async () => { | ||
const wrapper = mount({ | ||
components: { UIPagination }, | ||
template: '<UIPagination v-model="value" length="10" visible-count="3" />' + | ||
'<div id="decrement" @click="value--" />', | ||
setup: () => { | ||
return { | ||
value: ref(5) | ||
}; | ||
} | ||
}); | ||
|
||
expect(wrapper.find('#page-5-button').attributes()['title']).toBe('Current Page, Page 5'); | ||
await wrapper.find('#next-page-button').trigger('click'); | ||
expect(wrapper.find('#page-5-button').attributes()['title']).toBe('Go to page 5'); | ||
expect(wrapper.find('#page-6-button').attributes()['title']).toBe('Current Page, Page 6'); | ||
await wrapper.find('#decrement').trigger('click'); | ||
expect(wrapper.find('#page-5-button').attributes()['title']).toBe('Current Page, Page 5'); | ||
expect(wrapper.find('#page-6-button').attributes()['title']).toBe('Go to page 6'); | ||
}); | ||
|
||
it('should disable buttons if prop given', () => { | ||
const wrapper = mount(UIPagination, { | ||
props: { ...commonProps, disabled: true } | ||
}); | ||
|
||
expect(wrapper.findAll('button[disabled]')).toHaveLength( | ||
// previous and next buttons | ||
2 | ||
// the buttons | ||
+ commonProps.visibleCount | ||
// the wrapping end buttons | ||
+ 2 | ||
); | ||
}); | ||
}); |
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 |
---|---|---|
@@ -0,0 +1,174 @@ | ||
<template> | ||
<div :key="page" | ||
class="flex flex-wrap justify-between items-center max-w-max space-x-2" | ||
role="navigation" | ||
aria-label="Pagination navigation"> | ||
<UIButton id="previous-page-button" | ||
:disabled="disabled || !hasPrevious" | ||
aria-label="Previous Page" | ||
class="transform rotate-90 !p-1" | ||
@click="page === 1 ? undefined : setPage(page - 1)" | ||
v-html="chevronIcon" /> | ||
<UIButton id="page-1-button" | ||
:aria-current="isCurrent = page === 1" | ||
:category="isCurrent ? 'primary' : 'default'" | ||
:disabled="disabled" | ||
:outline="!isCurrent" | ||
class="page-button" | ||
:aria-label="isCurrent ? 'Current Page, Page 1' : 'Go to page 1'" | ||
:title="isCurrent ? 'Current Page, Page 1' : 'Go to page 1'" | ||
@click="setPage(1)"> | ||
1 | ||
</UIButton> | ||
<div v-if="pages[0] !== 2" class="select-none"> | ||
... | ||
</div> | ||
<UIButton v-for="pageNum in pages" | ||
:id="`page-${pageNum}-button`" | ||
:key="pageNum" | ||
:aria-current="isCurrent = pageNum === page" | ||
:category="isCurrent ? 'primary' : 'default'" | ||
:outline="!isCurrent" | ||
class="page-button" | ||
:aria-label="isCurrent ? 'Current Page, Page ' + pageNum : 'Go to page ' + pageNum" | ||
:title="isCurrent ? 'Current Page, Page ' + pageNum : 'Go to page ' + pageNum" | ||
:disabled="disabled" | ||
@click="setPage(pageNum)"> | ||
{{ pageNum }} | ||
</UIButton> | ||
<div v-if="startPagesFrom + Number(visibleCount) < Number(length)" class="select-none"> | ||
... | ||
</div> | ||
<UIButton v-if="Number(length) > 1" | ||
:id="`page-${length}-button`" | ||
:aria-current="isCurrent = page === Number(length)" | ||
:category="isCurrent ? 'primary' : 'default'" | ||
:disabled="disabled" | ||
class="page-button" | ||
:outline="!isCurrent" | ||
:aria-label="isCurrent ? 'Current Page, Page ' + length : 'Go to page ' + length" | ||
:title="isCurrent ? 'Current Page, Page ' + length : 'Go to page ' + length" | ||
@click="setPage(Number(length))"> | ||
{{ length }} | ||
</UIButton> | ||
<UIButton id="next-page-button" | ||
:disabled="disabled || !hasNext" | ||
aria-label="Next Page" | ||
class="transform rotate-270 !p-1" | ||
@click="page === Number(length) ? undefined : setPage(page + 1)" | ||
v-html="chevronIcon" /> | ||
</div> | ||
</template> | ||
|
||
<script lang="ts"> | ||
import { computed, defineComponent } from 'vue'; | ||
import { useVModel, disabled } from '@composables/input'; | ||
import { getIcon } from '@/helpers'; | ||
import UIButton from '@components/button/UIButton.vue'; | ||
export default defineComponent({ | ||
name: 'UIPagination', | ||
components: { UIButton }, | ||
props: { | ||
/** | ||
* The current page. | ||
*/ | ||
modelValue: { | ||
type: Number, | ||
default: 1, | ||
validator: (val: number) => Number.isInteger(val) && val >= 0 | ||
}, | ||
/** | ||
* The number of pages. | ||
*/ | ||
length: { | ||
type: [Number, String], | ||
required: true, | ||
validator: (val: number | string) => { | ||
val = Number(val); | ||
return !isNaN(val) && Number.isInteger(val) && val > 0; | ||
} | ||
}, | ||
/** | ||
* The total number of page buttons to show. | ||
*/ | ||
visibleCount: { | ||
type: [Number, String], | ||
default: 3, | ||
validator: (val: number | string) => { | ||
val = Number(val); | ||
const isValid = !isNaN(val) && Number.isInteger(val) && val >= 0; | ||
if (isValid && val % 2 !== 1) { | ||
throw new Error('visibleCount expected to be an odd number.'); | ||
} | ||
return isValid; | ||
} | ||
}, | ||
disabled | ||
}, | ||
emits: ['update:modelValue'], | ||
setup(props) { | ||
const chevronIcon = getIcon('chevron'); | ||
const page = useVModel<number>(props); | ||
const hasPrevious = computed(() => page.value > 1); | ||
const hasNext = computed(() => page.value < Number(props.length)); | ||
const startPagesFrom = computed(() => { | ||
const sideLength = Math.floor(Number(props.visibleCount) / 2); | ||
if (page.value <= sideLength + 1) { | ||
return 2; | ||
} | ||
if (page.value >= Number(props.length) - sideLength) { | ||
return Number(props.length) - Number(props.visibleCount); | ||
} | ||
if (Number(props.length) === Number(props.visibleCount)) { | ||
return page.value - sideLength - 1; | ||
} | ||
return page.value - sideLength; | ||
}); | ||
const pages = computed(() => { | ||
return Array.from( | ||
{ length: Number(props.visibleCount) }, | ||
(_, index) => index + startPagesFrom.value | ||
) | ||
.filter(pageNum => pageNum > 1 && pageNum < Number(props.length)); | ||
}); | ||
const setPage = (pageNum: number) => { | ||
if (pageNum === page.value) return; | ||
page.value = pageNum; | ||
}; | ||
return { | ||
page, | ||
hasPrevious, | ||
startPagesFrom, | ||
setPage, | ||
hasNext, | ||
chevronIcon, | ||
pages | ||
}; | ||
} | ||
}); | ||
</script> | ||
|
||
<style> | ||
.page-button { | ||
min-width: 40px; | ||
padding: 0 5px !important; | ||
height: 38px; | ||
} | ||
</style> |
Oops, something went wrong.