Skip to content

Commit

Permalink
#105 UIPagination: add component (#111)
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
nandi95 authored Jul 14, 2021
1 parent 0bf7240 commit 533ed51
Show file tree
Hide file tree
Showing 12 changed files with 749 additions and 235 deletions.
1 change: 1 addition & 0 deletions .commitlintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
'table',
'textarea',
'toggle',
'pagination',
'transitions',
'tooltip',
'outer-html',
Expand Down
464 changes: 232 additions & 232 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
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"
Expand Down
1 change: 1 addition & 0 deletions src/components/input/UIInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('UIInput', () => {

expect(wrapper.element).toMatchSnapshot();
});

it('should handle model-binding correctly', async () => {
const wrapper = mount(UIInput, {
props: {
Expand Down
53 changes: 53 additions & 0 deletions src/components/pagination/Demo.vue
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>
121 changes: 121 additions & 0 deletions src/components/pagination/UIPagination.test.ts
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
);
});
});
174 changes: 174 additions & 0 deletions src/components/pagination/UIPagination.vue
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>
Loading

0 comments on commit 533ed51

Please sign in to comment.