@kiva/kv-components 3.13.2 → 3.15.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,34 @@
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.15.0](https://github.com/kiva/kv-ui-elements/compare/@kiva/kv-components@3.14.0...@kiva/kv-components@3.15.0) (2023-04-27)
7
+
8
+
9
+ ### Features
10
+
11
+ * implemented initial kv-loan-filters package ([1987069](https://github.com/kiva/kv-ui-elements/commit/1987069227099a2ef1cabf09d1086e6a66adee0d))
12
+
13
+
14
+
15
+
16
+
17
+ # [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)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * call userEvent directly ([461774f](https://github.com/kiva/kv-ui-elements/commit/461774f4e6596030b806d03084486e961011f8be))
23
+
24
+
25
+ ### Features
26
+
27
+ * event guidelines ([37c1635](https://github.com/kiva/kv-ui-elements/commit/37c16351f8a29b3949745cc2786d394049bd6d5c))
28
+ * move KvPagination component to kv-elements ([5f24d6a](https://github.com/kiva/kv-ui-elements/commit/5f24d6aae4bdb2909fb61c666b6a3d64dd76da99))
29
+
30
+
31
+
32
+
33
+
6
34
  ## [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
35
 
8
36
  **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.15.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -47,10 +47,11 @@
47
47
  "storybook": "vue-demi-switch 2 && start-storybook -p 6006 -c vue/.storybook",
48
48
  "build-storybook": "vue-demi-switch 2 && build-storybook -c vue/.storybook",
49
49
  "lint": "eslint --ext .js,.vue ./",
50
- "test": "npm run lint"
50
+ "test": "npm run lint",
51
+ "build": "echo No build needed for @kiva/kv-components."
51
52
  },
52
53
  "dependencies": {
53
- "@kiva/kv-tokens": "^2.5.0",
54
+ "@kiva/kv-tokens": "^2.6.0",
54
55
  "@mdi/js": "^5.9.55",
55
56
  "@vueuse/integrations": "^7.6.0",
56
57
  "aria-hidden": "^1.1.3",
@@ -69,5 +70,5 @@
69
70
  "optional": true
70
71
  }
71
72
  },
72
- "gitHead": "4c0fd566bb167fb765adf4bd92dccb4bb0814059"
73
+ "gitHead": "a81343a324e4e21b58b06920bf74f63611efffdc"
73
74
  }
@@ -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
+ });
package/vue/KvButton.vue CHANGED
@@ -132,6 +132,7 @@ export default {
132
132
  if (state.value === 'active') {
133
133
  classes = `${classes} tw-bg-action-highlight tw-border-action-highlight`;
134
134
  } else {
135
+ // eslint-disable-next-line max-len
135
136
  classes = `${classes} tw-bg-action hover:tw-bg-action-highlight tw-border-action hover:tw-border-action-highlight`;
136
137
  }
137
138
  break;
@@ -140,6 +141,7 @@ export default {
140
141
  if (state.value === 'active') {
141
142
  classes = `${classes} tw-bg-secondary tw-border-primary`;
142
143
  } else {
144
+ // eslint-disable-next-line max-len
143
145
  classes = `${classes} tw-bg-primary hover:tw-bg-secondary tw-border-tertiary hover:tw-border-primary`;
144
146
  }
145
147
  break;
@@ -148,6 +150,7 @@ export default {
148
150
  if (state.value === 'active') {
149
151
  classes = `${classes} tw-bg-danger-highlight tw-border-danger-highlight`;
150
152
  } else {
153
+ // eslint-disable-next-line max-len
151
154
  classes = `${classes} tw-bg-danger hover:tw-bg-danger-highlight tw-border-danger hover:tw-border-danger-highlight`;
152
155
  }
153
156
  break;
@@ -170,6 +170,7 @@ export default {
170
170
  default: 'center',
171
171
  validator(value) {
172
172
  // The value must match one of these strings
173
+ // eslint-disable-next-line max-len
173
174
  return ['center', 'top', 'right', 'left', 'bottom', 'top_right', 'top_left', 'bottom_right', 'bottom_left', 'face', 'faces'].indexOf(value) !== -1;
174
175
  },
175
176
  },
@@ -143,7 +143,7 @@ import KvMaterialIcon from './KvMaterialIcon.vue';
143
143
 
144
144
  /**
145
145
  * Alert or a lightbox
146
- * Accesibility: https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
146
+ * Accessibility: https://www.w3.org/TR/wai-aria-practices-1.1/#dialog_modal
147
147
  *
148
148
  * - [x] Tab and Shift + Tab do not move focus outside the dialog
149
149
  * - [x] focus is initially set on the first focusable element (close button).
@@ -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 });