@kiva/kv-components 3.13.2 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [3.14.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.13.2...@kiva/kv-components@3.14.0) (2023-04-17)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* call userEvent directly ([461774f](https://github.com/kiva/kv-ui-elements/commit/461774f4e6596030b806d03084486e961011f8be))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* event guidelines ([37c1635](https://github.com/kiva/kv-ui-elements/commit/37c16351f8a29b3949745cc2786d394049bd6d5c))
|
|
17
|
+
* move KvPagination component to kv-elements ([5f24d6a](https://github.com/kiva/kv-ui-elements/commit/5f24d6aae4bdb2909fb61c666b6a3d64dd76da99))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
## [3.13.2](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.13.1...@kiva/kv-components@3.13.2) (2023-04-14)
|
|
7
24
|
|
|
8
25
|
**Note:** Version bump only for package @kiva/kv-components
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kiva/kv-components",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"optional": true
|
|
70
70
|
}
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "fb5b0f2e5f92a57add53608ac3eddb0e100620be"
|
|
73
73
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { render } from '@testing-library/vue';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import KvPagination from '../../../../vue/KvPagination.vue';
|
|
4
|
+
|
|
5
|
+
global.scrollTo = jest.fn();
|
|
6
|
+
|
|
7
|
+
describe('KvPagination', () => {
|
|
8
|
+
afterEach(jest.clearAllMocks);
|
|
9
|
+
|
|
10
|
+
it('should render arrows disabled by default', async () => {
|
|
11
|
+
const { getByLabelText, emitted } = render(KvPagination, { props: { limit: 10, total: 0 } });
|
|
12
|
+
|
|
13
|
+
await userEvent.click(getByLabelText('Previous page'));
|
|
14
|
+
await userEvent.click(getByLabelText('Next page'));
|
|
15
|
+
|
|
16
|
+
expect(emitted()['page-changed']).toBe(undefined);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should render aria current page label', () => {
|
|
20
|
+
const { getByText } = render(KvPagination, { props: { limit: 10, total: 30 } });
|
|
21
|
+
|
|
22
|
+
getByText("You're on page");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render fewer pages', () => {
|
|
26
|
+
const { getByLabelText } = render(KvPagination, { props: { limit: 10, total: 30 } });
|
|
27
|
+
|
|
28
|
+
getByLabelText('Page 1');
|
|
29
|
+
getByLabelText('Page 2');
|
|
30
|
+
getByLabelText('Page 3');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render more pages', () => {
|
|
34
|
+
const { getByLabelText } = render(KvPagination, { props: { limit: 10, total: 1000 } });
|
|
35
|
+
|
|
36
|
+
getByLabelText('Page 1');
|
|
37
|
+
getByLabelText('Page 2');
|
|
38
|
+
getByLabelText('Page 3');
|
|
39
|
+
getByLabelText('Page 4');
|
|
40
|
+
getByLabelText('Page 100');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should render fourth selected', () => {
|
|
44
|
+
const { getByLabelText } = render(
|
|
45
|
+
KvPagination, { props: { limit: 10, total: 1000, offset: 30 } },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
getByLabelText('Page 1');
|
|
49
|
+
getByLabelText('Page 3');
|
|
50
|
+
getByLabelText('Page 4');
|
|
51
|
+
getByLabelText('Page 5');
|
|
52
|
+
getByLabelText('Page 100');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should render last selected', () => {
|
|
56
|
+
const { getByLabelText } = render(
|
|
57
|
+
KvPagination, { props: { limit: 10, total: 1000, offset: 990 } },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
getByLabelText('Page 1');
|
|
61
|
+
getByLabelText('Page 97');
|
|
62
|
+
getByLabelText('Page 98');
|
|
63
|
+
getByLabelText('Page 99');
|
|
64
|
+
getByLabelText('Page 100');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should render fourth to last last selected', () => {
|
|
68
|
+
const { getByLabelText } = render(
|
|
69
|
+
KvPagination, { props: { limit: 10, total: 1000, offset: 960 } },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
getByLabelText('Page 1');
|
|
73
|
+
getByLabelText('Page 96');
|
|
74
|
+
getByLabelText('Page 97');
|
|
75
|
+
getByLabelText('Page 98');
|
|
76
|
+
getByLabelText('Page 100');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should render more extra pages', () => {
|
|
80
|
+
const { getByLabelText } = render(
|
|
81
|
+
KvPagination, { props: { limit: 10, total: 1000, extraPages: 6 } },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
getByLabelText('Page 1');
|
|
85
|
+
getByLabelText('Page 2');
|
|
86
|
+
getByLabelText('Page 3');
|
|
87
|
+
getByLabelText('Page 4');
|
|
88
|
+
getByLabelText('Page 5');
|
|
89
|
+
getByLabelText('Page 6');
|
|
90
|
+
getByLabelText('Page 7');
|
|
91
|
+
getByLabelText('Page 100');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should emit page click', async () => {
|
|
95
|
+
const { getByLabelText, emitted } = render(
|
|
96
|
+
KvPagination, { props: { limit: 10, total: 1000 } },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await userEvent.click(getByLabelText('Page 2'));
|
|
100
|
+
|
|
101
|
+
expect(global.scrollTo).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(emitted()['page-changed']).toEqual([[{ pageOffset: 10 }]]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should not emit current page click', async () => {
|
|
106
|
+
const { getByLabelText, emitted } = render(KvPagination, { props: { limit: 10, total: 1000 } });
|
|
107
|
+
|
|
108
|
+
await userEvent.click(getByLabelText('Page 1'));
|
|
109
|
+
|
|
110
|
+
expect(emitted()['page-changed']).toBe(undefined);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should emit previous click', async () => {
|
|
114
|
+
const { getByLabelText, emitted } = render(
|
|
115
|
+
KvPagination, { props: { limit: 10, total: 1000, offset: 10 } },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
await userEvent.click(getByLabelText('Previous page'));
|
|
119
|
+
|
|
120
|
+
expect(global.scrollTo).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(emitted()['page-changed']).toEqual([[{ pageOffset: 0 }]]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should emit next click', async () => {
|
|
125
|
+
const { getByLabelText, emitted } = render(KvPagination, { props: { limit: 10, total: 1000 } });
|
|
126
|
+
|
|
127
|
+
await userEvent.click(getByLabelText('Next page'));
|
|
128
|
+
|
|
129
|
+
expect(global.scrollTo).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(emitted()['page-changed']).toEqual([[{ pageOffset: 10 }]]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav aria-label="Pagination">
|
|
3
|
+
<ul
|
|
4
|
+
class="tw-text-center tw-mx-auto tw-my-1.5 tw-flex tw-justify-between tw-items-center"
|
|
5
|
+
style="max-width: 17rem;"
|
|
6
|
+
>
|
|
7
|
+
<li>
|
|
8
|
+
<a
|
|
9
|
+
class="tw-cursor-pointer tw-flex"
|
|
10
|
+
:class="linkClass(0)"
|
|
11
|
+
aria-label="Previous page"
|
|
12
|
+
@click="!isCurrent(0) && clickPrevious()"
|
|
13
|
+
>
|
|
14
|
+
<kv-material-icon
|
|
15
|
+
:icon="mdiChevronLeft"
|
|
16
|
+
class="tw-h-4 tw-w-4"
|
|
17
|
+
/>
|
|
18
|
+
<span class="tw-sr-only">Previous page</span>
|
|
19
|
+
</a>
|
|
20
|
+
</li>
|
|
21
|
+
<li
|
|
22
|
+
v-for="(n, i) in numbers"
|
|
23
|
+
:key="i"
|
|
24
|
+
:aria-hidden="isEllipsis(n)"
|
|
25
|
+
>
|
|
26
|
+
<template v-if="isEllipsis(n)">
|
|
27
|
+
...
|
|
28
|
+
</template>
|
|
29
|
+
<a
|
|
30
|
+
v-else
|
|
31
|
+
class="tw-cursor-pointer"
|
|
32
|
+
:class="linkClass(n)"
|
|
33
|
+
:aria-label="`Page ${n + 1}`"
|
|
34
|
+
@click="!isCurrent(n) && clickPage(n)"
|
|
35
|
+
>
|
|
36
|
+
<span
|
|
37
|
+
v-if="isCurrent(n)"
|
|
38
|
+
class="tw-sr-only"
|
|
39
|
+
>You're on page</span>
|
|
40
|
+
{{ n + 1 }}
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
<li>
|
|
44
|
+
<a
|
|
45
|
+
class="tw-cursor-pointer tw-flex"
|
|
46
|
+
:class="linkClass(totalPages ? totalPages - 1 : 0)"
|
|
47
|
+
aria-label="Next page"
|
|
48
|
+
@click="totalPages && !isCurrent(totalPages - 1) && clickNext()"
|
|
49
|
+
>
|
|
50
|
+
<kv-material-icon
|
|
51
|
+
:icon="mdiChevronRight"
|
|
52
|
+
class="tw-h-4 tw-w-4"
|
|
53
|
+
/>
|
|
54
|
+
<span class="tw-sr-only">Next page</span>
|
|
55
|
+
</a>
|
|
56
|
+
</li>
|
|
57
|
+
</ul>
|
|
58
|
+
</nav>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script>
|
|
62
|
+
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
|
63
|
+
import KvMaterialIcon from './KvMaterialIcon.vue';
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
name: 'KvPagination',
|
|
67
|
+
components: {
|
|
68
|
+
KvMaterialIcon,
|
|
69
|
+
},
|
|
70
|
+
props: {
|
|
71
|
+
limit: {
|
|
72
|
+
type: Number,
|
|
73
|
+
required: true,
|
|
74
|
+
validator: (value) => value > 0,
|
|
75
|
+
},
|
|
76
|
+
total: {
|
|
77
|
+
type: Number,
|
|
78
|
+
required: true,
|
|
79
|
+
validator: (value) => value >= 0,
|
|
80
|
+
},
|
|
81
|
+
offset: {
|
|
82
|
+
type: Number,
|
|
83
|
+
default: 0,
|
|
84
|
+
validator: (value) => value >= 0,
|
|
85
|
+
},
|
|
86
|
+
extraPages: {
|
|
87
|
+
type: Number,
|
|
88
|
+
default: 3,
|
|
89
|
+
validator: (value) => value > 0,
|
|
90
|
+
},
|
|
91
|
+
scrollToTop: {
|
|
92
|
+
type: Boolean,
|
|
93
|
+
default: true,
|
|
94
|
+
},
|
|
95
|
+
kvTrackFunction: {
|
|
96
|
+
type: Function,
|
|
97
|
+
default: () => {},
|
|
98
|
+
},
|
|
99
|
+
trackEventCategory: {
|
|
100
|
+
type: String,
|
|
101
|
+
default: '',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
data() {
|
|
105
|
+
return {
|
|
106
|
+
mdiChevronLeft,
|
|
107
|
+
mdiChevronRight,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
computed: {
|
|
111
|
+
current() {
|
|
112
|
+
const page = this.offset / this.limit;
|
|
113
|
+
|
|
114
|
+
// This component uses a 0-based page index
|
|
115
|
+
return page < this.totalPages ? page : 0;
|
|
116
|
+
},
|
|
117
|
+
totalPages() {
|
|
118
|
+
return Math.ceil(this.total / this.limit);
|
|
119
|
+
},
|
|
120
|
+
numbers() {
|
|
121
|
+
// If less than the max, there will be no ellipsis, so just return the numbers
|
|
122
|
+
if (this.totalPages < (this.extraPages + 2)) {
|
|
123
|
+
return this.range(0, this.totalPages - 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const numbers = [];
|
|
127
|
+
|
|
128
|
+
// Add the 'middle' block of numbers based upon the current page
|
|
129
|
+
if ([0, 1, 2].includes(this.current)) {
|
|
130
|
+
numbers.push(...this.range(1, this.extraPages));
|
|
131
|
+
} else if ([this.totalPages - 3, this.totalPages - 2, this.totalPages - 1]
|
|
132
|
+
.includes(this.current)) {
|
|
133
|
+
numbers.push(...this.range(this.totalPages - this.extraPages - 1, this.totalPages - 2));
|
|
134
|
+
} else {
|
|
135
|
+
const delta = Math.floor(this.extraPages / 2);
|
|
136
|
+
numbers.push(...this.range(this.current - delta, this.current + delta));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add a placeholder for first ellipsis
|
|
140
|
+
if (numbers[1] !== 2) {
|
|
141
|
+
numbers.splice(0, 0, -1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add a placeholder for second ellipsis
|
|
145
|
+
const totalNumbers = numbers.length;
|
|
146
|
+
if (numbers[totalNumbers - 1] !== this.totalPages - 2) {
|
|
147
|
+
numbers.splice(totalNumbers, 0, -1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add first and last pages
|
|
151
|
+
numbers.unshift(0);
|
|
152
|
+
numbers.push(this.totalPages - 1);
|
|
153
|
+
|
|
154
|
+
return numbers;
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
methods: {
|
|
158
|
+
range(start, end) {
|
|
159
|
+
return [...Array(end - start + 1)].map((_, n) => n + start);
|
|
160
|
+
},
|
|
161
|
+
isCurrent(number) {
|
|
162
|
+
return number === this.current;
|
|
163
|
+
},
|
|
164
|
+
isEllipsis(number) {
|
|
165
|
+
return number === -1;
|
|
166
|
+
},
|
|
167
|
+
linkClass(number) {
|
|
168
|
+
return { 'tw-text-tertiary': this.isCurrent(number), 'tw-pointer-events-none': this.isCurrent(number) };
|
|
169
|
+
},
|
|
170
|
+
pageChange(number) {
|
|
171
|
+
if (this.scrollToTop && window.scrollTo) {
|
|
172
|
+
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.$emit('page-changed', { pageOffset: number * this.limit });
|
|
176
|
+
},
|
|
177
|
+
clickPage(number) {
|
|
178
|
+
this.pageChange(number);
|
|
179
|
+
|
|
180
|
+
this.kvTrackFunction(this.trackEventCategory, 'click', 'pagination-number', null, number + 1);
|
|
181
|
+
},
|
|
182
|
+
clickPrevious() {
|
|
183
|
+
const previous = this.current - 1;
|
|
184
|
+
|
|
185
|
+
this.pageChange(previous);
|
|
186
|
+
|
|
187
|
+
this.kvTrackFunction(this.trackEventCategory, 'click', 'pagination-previous', null, previous + 1);
|
|
188
|
+
},
|
|
189
|
+
clickNext() {
|
|
190
|
+
const next = this.current + 1;
|
|
191
|
+
|
|
192
|
+
this.pageChange(next);
|
|
193
|
+
|
|
194
|
+
this.kvTrackFunction(this.trackEventCategory, 'click', 'pagination-next', null, next + 1);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
</script>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import KvPagination from '../KvPagination.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'KvPagination',
|
|
5
|
+
component: KvPagination,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const story = (args) => {
|
|
9
|
+
const template = (_args, { argTypes }) => ({
|
|
10
|
+
props: Object.keys(argTypes),
|
|
11
|
+
components: { KvPagination },
|
|
12
|
+
template: `<kv-pagination
|
|
13
|
+
:limit="limit"
|
|
14
|
+
:total="total"
|
|
15
|
+
:offset="offset"
|
|
16
|
+
:extra-pages="extraPages"
|
|
17
|
+
:kv-track-function="kvTrackMock"
|
|
18
|
+
:track-event-category="trackEventCategory"
|
|
19
|
+
/>`,
|
|
20
|
+
methods: {
|
|
21
|
+
kvTrackMock(
|
|
22
|
+
category,
|
|
23
|
+
action,
|
|
24
|
+
label,
|
|
25
|
+
property,
|
|
26
|
+
value,
|
|
27
|
+
) {
|
|
28
|
+
console.log(category, action, label, property, value);
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
template.args = args;
|
|
33
|
+
return template;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Default = story({ limit: 10, total: 0, trackEventCategory: 'blog-articles' });
|
|
37
|
+
|
|
38
|
+
export const FewerPages = story({ limit: 10, total: 30, trackEventCategory: 'blog-articles' });
|
|
39
|
+
|
|
40
|
+
export const MorePages = story({ limit: 10, total: 1000, trackEventCategory: 'blog-articles' });
|
|
41
|
+
|
|
42
|
+
export const SecondSelected = story({
|
|
43
|
+
limit: 10, total: 1000, offset: 10, trackEventCategory: 'blog-articles',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const ThirdSelected = story({
|
|
47
|
+
limit: 10, total: 1000, offset: 20, trackEventCategory: 'blog-articles',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const FourthSelected = story({
|
|
51
|
+
limit: 10, total: 1000, offset: 30, trackEventCategory: 'blog-articles',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const LastSelected = story({
|
|
55
|
+
limit: 10, total: 1000, offset: 990, trackEventCategory: 'blog-articles',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const SecondToLastSelected = story({
|
|
59
|
+
limit: 10, total: 1000, offset: 980, trackEventCategory: 'blog-articles',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const ThirdToLastSelected = story({
|
|
63
|
+
limit: 10, total: 1000, offset: 970, trackEventCategory: 'blog-articles',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const FourthToLastSelected = story({
|
|
67
|
+
limit: 10, total: 1000, offset: 960, trackEventCategory: 'blog-articles',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const MoreExtraPages = story({ limit: 10, total: 1000, extraPages: 6 });
|