@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.13.2",
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": "4c0fd566bb167fb765adf4bd92dccb4bb0814059"
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 });