@carbon/vue 3.0.31 → 3.0.32

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "@carbon/vue",
4
- "version": "3.0.30",
4
+ "version": "3.0.31",
5
5
  "contributions": {
6
6
  "html": {
7
7
  "description-markup": "markdown",
@@ -3503,6 +3503,14 @@
3503
3503
  },
3504
3504
  "default": "undefined"
3505
3505
  },
3506
+ {
3507
+ "name": "pageSize",
3508
+ "value": {
3509
+ "kind": "expression",
3510
+ "type": "number"
3511
+ },
3512
+ "default": "undefined"
3513
+ },
3506
3514
  {
3507
3515
  "name": "pageSizes",
3508
3516
  "value": {
@@ -3510,11 +3518,33 @@
3510
3518
  "type": "array"
3511
3519
  },
3512
3520
  "default": "[10, 20, 30, 40, 50]"
3521
+ },
3522
+ {
3523
+ "name": "rangeTextFormatter",
3524
+ "value": {
3525
+ "kind": "expression",
3526
+ "type": "func"
3527
+ },
3528
+ "default": "undefined"
3529
+ },
3530
+ {
3531
+ "name": "pageOfPagesFormatter",
3532
+ "value": {
3533
+ "kind": "expression",
3534
+ "type": "func"
3535
+ },
3536
+ "default": "undefined"
3513
3537
  }
3514
3538
  ],
3515
3539
  "events": [
3516
3540
  {
3517
3541
  "name": "change"
3542
+ },
3543
+ {
3544
+ "name": "update:page"
3545
+ },
3546
+ {
3547
+ "name": "update:pageSize"
3518
3548
  }
3519
3549
  ],
3520
3550
  "slots": [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@carbon/vue",
3
3
  "license": "Apache-2.0",
4
- "version": "3.0.31",
4
+ "version": "3.0.32",
5
5
  "description": "A collection of components for the Carbon Design System built using Vue.js",
6
6
  "packageManager": "yarn@3.2.0",
7
7
  "main": "dist/carbon-vue-3.umd.min.js",
@@ -296,6 +296,7 @@ const emit = defineEmits(['update:modelValue', 'change']);
296
296
  watch(
297
297
  () => props.modelValue,
298
298
  () => {
299
+ if (props.modelValue === dataValue.value) return;
299
300
  dataValue.value = props.modelValue;
300
301
  dataCaption.value = undefined;
301
302
  }
@@ -14,6 +14,7 @@
14
14
  tabindex="-1"
15
15
  v-bind="$attrs"
16
16
  @keydown.esc.prevent="onEsc"
17
+ @mousedown.self="onMouseDown"
17
18
  @click.self="onExternalClick"
18
19
  >
19
20
  <div
@@ -145,6 +146,8 @@ import {
145
146
  watch,
146
147
  } from 'vue';
147
148
 
149
+ const mouseDownTarget = ref(null);
150
+
148
151
  const props = defineProps({
149
152
  /**
150
153
  * When set to true, the aria role will be either "alert" or "alertdialog"
@@ -269,6 +272,7 @@ function show() {
269
272
 
270
273
  el.value?.addEventListener('transitionend', onShown);
271
274
  dataVisible.value = true;
275
+ mouseDownTarget.value = null;
272
276
  }
273
277
  const content = ref(null);
274
278
  function onShown() {
@@ -326,6 +330,7 @@ function hide() {
326
330
  }
327
331
 
328
332
  dataVisible.value = false;
333
+ mouseDownTarget.value = null;
329
334
  }
330
335
  function afterHide(event) {
331
336
  if (event.propertyName === 'opacity') {
@@ -375,10 +380,13 @@ function focusAfterContent() {
375
380
  }
376
381
 
377
382
  function onExternalClick(ev) {
378
- if (ev.target === el.value) {
383
+ if (ev.target === el.value && mouseDownTarget.value === el.value) {
379
384
  _maybeHide(ev, 'external-click');
380
385
  }
381
386
  }
387
+ function onMouseDown(ev) {
388
+ mouseDownTarget.value = ev.target;
389
+ }
382
390
  function onEsc(ev) {
383
391
  _maybeHide(ev, 'escape-press');
384
392
  }
@@ -3,9 +3,29 @@ import { CvPagination } from '.';
3
3
  import { sbCompPrefix } from '../../global/storybook-utils';
4
4
  import { action } from '@storybook/addon-actions';
5
5
  import { ref } from 'vue';
6
- const myPage=ref(1);
7
- const myPageSizes=ref([5,7,10,11,13])
8
- const myNumItems=ref(13)
6
+ const myPage = ref(1);
7
+ const myPageSizes = ref([5, 7, 10, 11, 13]);
8
+ const myNumItems = ref(13);
9
+ const controlledPage = ref(1);
10
+ const controlledPageSize = ref(10);
11
+ const maxAllowedPage = ref(3);
12
+ const onUpdatePageAction = action('update:page');
13
+ const onUpdatePageSizeAction = action('update:page-size');
14
+ function onUpdatePage(page) {
15
+ onUpdatePageAction(page);
16
+ // simulate a parent that constrains the page: requests past
17
+ // maxAllowedPage are declined by simply not assigning them back.
18
+ if (page <= maxAllowedPage.value) {
19
+ controlledPage.value = page;
20
+ }
21
+ }
22
+ function onUpdatePageSize(pageSize) {
23
+ onUpdatePageSizeAction(pageSize);
24
+ controlledPageSize.value = pageSize;
25
+ }
26
+ const spanishRangeTextFormatter = ({ start, end, items }) =>
27
+ `Del ${start} al ${end} de ${items}`;
28
+ const germanPageOfPagesFormatter = ({ pages }) => `von ${pages} Seiten`;
9
29
 
10
30
  <Meta title={`${sbCompPrefix}/CvPagination`} component={CvPagination} />
11
31
 
@@ -21,6 +41,13 @@ export const Template = args => ({
21
41
  myPage,
22
42
  myPageSizes,
23
43
  myNumItems,
44
+ controlledPage,
45
+ controlledPageSize,
46
+ maxAllowedPage,
47
+ onUpdatePage,
48
+ onUpdatePageSize,
49
+ spanishRangeTextFormatter,
50
+ germanPageOfPagesFormatter,
24
51
  onChange: action('change'),
25
52
  };
26
53
  },
@@ -75,10 +102,42 @@ const slotsTemplate = `
75
102
  </template>
76
103
  </cv-pagination>
77
104
  `;
105
+ const controlledTemplate = `
106
+ <cv-pagination
107
+ @change="onChange"
108
+ v-model:page="controlledPage"
109
+ v-model:page-size="controlledPageSize"
110
+ @update:page="onUpdatePage"
111
+ @update:page-size="onUpdatePageSize"
112
+ :number-of-items="103">
113
+ </cv-pagination>
114
+ <div style="margin-top:2rem; background-color: #888888; padding:1rem">
115
+ <h3>Parent-owned state</h3>
116
+ <p>page: {{controlledPage}}, page size: {{controlledPageSize}}</p>
117
+ <p>
118
+ Requests for a page beyond max-allowed-page are declined by this
119
+ story (the parent simply doesn't assign the requested page back) -
120
+ the displayed page stays put instead of drifting or getting stuck.
121
+ </p>
122
+ max allowed page:
123
+ <button @click="maxAllowedPage--">-</button>
124
+ {{maxAllowedPage}}
125
+ <button @click="maxAllowedPage++">+</button>
126
+ </div>
127
+ `;
128
+ const formattersTemplate = `
129
+ <cv-pagination
130
+ @change="onChange"
131
+ :number-of-items="103"
132
+ :range-text-formatter="spanishRangeTextFormatter"
133
+ :page-of-pages-formatter="germanPageOfPagesFormatter">
134
+ </cv-pagination>
135
+ `;
78
136
 
79
137
  # CvPagination
80
138
 
81
139
  Migration notes:
140
+
82
141
  - If you specify `pageNumberLabel` a trailing colon (:) is no longer automatically appended to the text
83
142
 
84
143
  <Canvas>
@@ -111,14 +170,13 @@ Migration notes:
111
170
  argTypes={{
112
171
  page: { control: false },
113
172
  pageSizes: { control: false },
114
- numberOfItems: { control: false }
173
+ numberOfItems: { control: false },
115
174
  }}
116
175
  >
117
176
  {Template.bind({})}
118
177
  </Story>
119
178
  </Canvas>
120
179
 
121
-
122
180
  Use slots to set the range text and "of n pages" text. A good use case for this is internationalization.
123
181
 
124
182
  <Canvas>
@@ -126,13 +184,7 @@ Use slots to set the range text and "of n pages" text. A good use case for this
126
184
  name="Slots"
127
185
  parameters={{
128
186
  controls: {
129
- exclude: [
130
- 'default',
131
- 'template',
132
- 'data',
133
- 'change',
134
- 'of-n-pages',
135
- ],
187
+ exclude: ['default', 'template', 'data', 'change', 'of-n-pages'],
136
188
  },
137
189
  docs: { source: { code: slotsTemplate } },
138
190
  }}
@@ -158,10 +210,71 @@ Use slots to set the range text and "of n pages" text. A good use case for this
158
210
  backwardsButtonDisabled: false,
159
211
  forwardsButtonDisabled: false,
160
212
  actualItemsOnPage: Infinity,
161
- locale: 'en'
213
+ locale: 'en',
162
214
  }}
163
- argTypes={{
215
+ argTypes={{}}
216
+ >
217
+ {Template.bind({})}
218
+ </Story>
219
+ </Canvas>
220
+
221
+ Bind `v-model:page` and/or `v-model:page-size` to opt in to controlled mode:
222
+ the component no longer moves on its own, it emits `update:page` /
223
+ `update:page-size` and only shows the new value once the parent assigns it
224
+ back through the prop. This lets a parent veto a page change - useful when
225
+ loading the next page's data can fail, or is gated behind other state.
226
+
227
+ <Canvas>
228
+ <Story
229
+ name="Controlled"
230
+ parameters={{
231
+ controls: {
232
+ exclude: [
233
+ 'default',
234
+ 'template',
235
+ 'data',
236
+ 'range-text',
237
+ 'change',
238
+ 'of-n-pages',
239
+ ],
240
+ },
241
+ docs: { source: { code: controlledTemplate } },
242
+ }}
243
+ args={{
244
+ data: {},
245
+ template: controlledTemplate,
246
+ }}
247
+ argTypes={{}}
248
+ >
249
+ {Template.bind({})}
250
+ </Story>
251
+ </Canvas>
252
+
253
+ Use `rangeTextFormatter` and `pageOfPagesFormatter` to translate the range
254
+ and page-count text without reimplementing the `range-text` / `of-n-pages`
255
+ slots.
256
+
257
+ <Canvas>
258
+ <Story
259
+ name="Formatters"
260
+ parameters={{
261
+ controls: {
262
+ exclude: [
263
+ 'default',
264
+ 'template',
265
+ 'data',
266
+ 'range-text',
267
+ 'change',
268
+ 'of-n-pages',
269
+ ],
270
+ },
271
+ docs: { source: { code: formattersTemplate } },
272
+ }}
273
+ args={{
274
+ data: {},
275
+ template: formattersTemplate,
164
276
  }}
277
+ argTypes={{}}
165
278
  >
166
279
  {Template.bind({})}
167
280
  </Story>
@@ -84,13 +84,20 @@
84
84
 
85
85
  <script setup>
86
86
  import { carbonPrefix } from '../../global/settings';
87
- import { computed, nextTick, onMounted, ref, useAttrs, watch } from 'vue';
87
+ import {
88
+ computed,
89
+ getCurrentInstance,
90
+ onMounted,
91
+ ref,
92
+ useAttrs,
93
+ watch,
94
+ } from 'vue';
88
95
  import { CaretLeft16, CaretRight16 } from '@carbon/icons-vue';
89
96
  import CvSelect from '../CvSelect';
90
97
  import CvSelectOption from '../CvSelect/CvSelectOption.vue';
91
98
  import { useCvId } from '../../use/cvId';
92
99
 
93
- const emit = defineEmits(['change']);
100
+ const emit = defineEmits(['change', 'update:page', 'update:pageSize']);
94
101
 
95
102
  const props = defineProps({
96
103
  backwardsButtonDisabled: { type: Boolean, default: false },
@@ -102,17 +109,38 @@ const props = defineProps({
102
109
  numberOfItems: { type: Number, default: Infinity },
103
110
  actualItemsOnPage: { type: Number, default: Infinity },
104
111
  page: { type: Number, default: undefined },
112
+ pageSize: { type: Number, default: undefined },
105
113
  pageSizes: { type: Array, default: () => [10, 20, 30, 40, 50] },
114
+ rangeTextFormatter: { type: Function, default: undefined },
115
+ pageOfPagesFormatter: { type: Function, default: undefined },
106
116
  });
107
117
  const attrs = useAttrs();
108
118
  const cvId = useCvId(attrs, true);
109
119
 
120
+ // Read once at setup time: whether the parent bound a v-model listener for
121
+ // this prop. vnode.props is a plain object Vue replaces on patch, not
122
+ // something Vue's reactivity system tracks, so this can't be a computed()
123
+ // that reacts to listeners being added/removed later - it only reflects
124
+ // how the component was initially used.
125
+ const instance = getCurrentInstance();
126
+ const pageControlled = 'onUpdate:page' in (instance.vnode.props || {});
127
+ const pageSizeControlled = 'onUpdate:pageSize' in (instance.vnode.props || {});
128
+
110
129
  const firstItem = ref(1);
111
- const pageValue = ref(1);
112
- const pageSizeValue = ref(10);
130
+ const internalPage = ref(1);
131
+ const internalPageSize = ref(10);
113
132
  const pageCount = ref(1);
114
133
  const pages = ref([1]);
115
134
 
135
+ const pageValue = computed(() =>
136
+ pageControlled && props.page !== undefined ? props.page : internalPage.value
137
+ );
138
+ const pageSizeValue = computed(() =>
139
+ pageSizeControlled && props.pageSize !== undefined
140
+ ? props.pageSize
141
+ : internalPageSize.value
142
+ );
143
+
116
144
  const noWayBack = computed(() => {
117
145
  return props.backwardsButtonDisabled || pageValue.value === 1;
118
146
  });
@@ -127,6 +155,9 @@ const ofNPagesProps = computed(() => {
127
155
  });
128
156
  const pageOfPages = computed(() => {
129
157
  const { pages, items } = ofNPagesProps.value;
158
+ if (props.pageOfPagesFormatter) {
159
+ return props.pageOfPagesFormatter({ pages, items, page: pageValue.value });
160
+ }
130
161
  if (items !== Infinity) {
131
162
  return `of ${pages} pages`;
132
163
  }
@@ -147,6 +178,9 @@ const rangeProps = computed(() => {
147
178
  const rangeText = computed(() => {
148
179
  const { start, end, items } = rangeProps.value;
149
180
 
181
+ if (props.rangeTextFormatter) {
182
+ return props.rangeTextFormatter({ start, end, items });
183
+ }
150
184
  if (items !== Infinity) {
151
185
  return `${start}-${end} of ${items} items`;
152
186
  } else {
@@ -162,16 +196,23 @@ const internalValue = computed(() => {
162
196
  });
163
197
 
164
198
  function adjustValues() {
165
- pageSizeValue.value = newPageSizeValue(props.pageSizes);
199
+ if (!pageSizeControlled) {
200
+ internalPageSize.value = newPageSizeValue(props.pageSizes);
201
+ }
166
202
  pageCount.value = newPageCount(props.numberOfItems, pageSizeValue.value);
167
- pageValue.value = newPageValue(props.page, pageCount.value);
203
+ if (!pageControlled) {
204
+ internalPage.value = newPageValue(props.page, pageCount.value);
205
+ }
168
206
  pages.value = newPagesArray(pageCount.value);
169
207
  firstItem.value = newFirstItem(pageValue.value, pageSizeValue.value);
170
208
  }
171
209
  onMounted(() => {
172
210
  adjustValues();
173
- // always emit on mount
174
- emit('change', internalValue.value);
211
+ // always emit on mount, unless both page and pageSize are fully
212
+ // controlled by the parent, in which case nothing here was derived.
213
+ if (!pageControlled || !pageSizeControlled) {
214
+ emit('change', internalValue.value);
215
+ }
175
216
  });
176
217
  watch(
177
218
  () => props.pageSizes,
@@ -186,40 +227,46 @@ watch(
186
227
  () => adjustValues()
187
228
  );
188
229
 
230
+ function propose(nextPage, nextPageSize) {
231
+ if (!pageControlled) {
232
+ internalPage.value = nextPage;
233
+ }
234
+ if (!pageSizeControlled) {
235
+ internalPageSize.value = nextPageSize;
236
+ }
237
+ firstItem.value = newFirstItem(nextPage, nextPageSize);
238
+
239
+ if (pageControlled && nextPage !== pageValue.value) {
240
+ emit('update:page', nextPage);
241
+ }
242
+ if (pageSizeControlled && nextPageSize !== pageSizeValue.value) {
243
+ emit('update:pageSize', nextPageSize);
244
+ }
245
+ emit('change', {
246
+ start: firstItem.value,
247
+ page: nextPage,
248
+ length: nextPageSize,
249
+ });
250
+ }
189
251
  function onPageChange(newVal) {
190
- pageValue.value = parseInt(newVal, 10);
191
- firstItem.value = newFirstItem(pageValue.value, pageSizeValue.value);
192
- emit('change', internalValue.value);
252
+ propose(parseInt(newVal, 10), pageSizeValue.value);
193
253
  }
194
- watch(
195
- () => props.numberOfItems,
196
- () => onPageSizeChange(pageSizeValue.value)
197
- );
198
254
  function onPageSizeChange(newVal) {
199
- pageSizeValue.value = parseInt(newVal, 10);
200
- pageCount.value = newPageCount(props.numberOfItems, pageSizeValue.value);
201
- pages.value = newPagesArray(pageCount.value);
202
- // what page is firstItem on
203
- nextTick(() => {
204
- // setting pageValue immediately seems to cause a problem - test set pageSize to 40, page to 3, set pageSize to 10
205
- // this previously resulted in 1 being set on Chrome (other browsers untested)
206
- pageValue.value = Math.ceil(firstItem.value / pageSizeValue.value);
207
- firstItem.value = newFirstItem(pageValue.value, pageSizeValue.value);
208
- emit('change', internalValue.value);
209
- });
255
+ const nextPageSize = parseInt(newVal, 10);
256
+ const nextPageCount = newPageCount(props.numberOfItems, nextPageSize);
257
+ pageCount.value = nextPageCount;
258
+ pages.value = newPagesArray(nextPageCount);
259
+ const nextPage = Math.ceil(firstItem.value / nextPageSize);
260
+ propose(nextPage, nextPageSize);
210
261
  }
211
262
  function onPrevPage() {
212
263
  if (pageValue.value > 1) {
213
- pageValue.value--;
214
- firstItem.value = newFirstItem(pageValue.value, pageSizeValue.value);
215
- emit('change', internalValue.value);
264
+ propose(pageValue.value - 1, pageSizeValue.value);
216
265
  }
217
266
  }
218
267
  function onNextPage() {
219
268
  if (pageValue.value < pageCount.value) {
220
- pageValue.value++;
221
- firstItem.value = newFirstItem(pageValue.value, pageSizeValue.value);
222
- emit('change', internalValue.value);
269
+ propose(pageValue.value + 1, pageSizeValue.value);
223
270
  }
224
271
  }
225
272
  function newPageValue(page, lastPage) {