@gitlab/ui 52.6.1 → 52.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "52.6.1",
3
+ "version": "52.7.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -327,7 +327,6 @@ export const glDropdownWidth = '15rem'
327
327
  export const glDropdownWidthNarrow = '10rem'
328
328
  export const glDropdownWidthWide = '25rem'
329
329
  export const glMaxDropdownMaxHeight = '19.5rem'
330
- export const glBroadcastMessageTextMaxWidth = '58.375rem'
331
330
  export const glBroadcastMessageNotificationMaxWidth = '18.75rem'
332
331
  export const glModalSmallWidth = '32rem'
333
332
  export const glModalMediumWidth = '48rem'
@@ -1745,11 +1745,6 @@
1745
1745
  "value": "px-to-rem(312px)",
1746
1746
  "compiledValue": "19.5rem"
1747
1747
  },
1748
- {
1749
- "name": "$gl-broadcast-message-text-max-width",
1750
- "value": "px-to-rem(934px)",
1751
- "compiledValue": "58.375rem"
1752
- },
1753
1748
  {
1754
1749
  "name": "$gl-broadcast-message-notification-max-width",
1755
1750
  "value": "px-to-rem(300px)",
@@ -68,10 +68,6 @@
68
68
  @include gl-justify-content-center;
69
69
  }
70
70
 
71
- &-text {
72
- max-width: $gl-broadcast-message-text-max-width;
73
- }
74
-
75
71
  &-icon {
76
72
  @include gl-mr-5;
77
73
 
@@ -79,7 +79,7 @@ export default {
79
79
  <div class="gl-broadcast-message-icon gl-line-height-normal">
80
80
  <gl-icon :name="iconName" />
81
81
  </div>
82
- <div class="gl-broadcast-message-text gl-my-n1">
82
+ <div class="gl-my-n1">
83
83
  <!-- @slot The broadcast message's text -->
84
84
  <slot></slot>
85
85
  </div>
@@ -1,5 +1,6 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { nextTick } from 'vue';
3
+ import { useMockIntersectionObserver } from '~/utils/use_mock_intersection_observer';
3
4
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
4
5
  import {
5
6
  GL_DROPDOWN_SHOWN,
@@ -9,6 +10,7 @@ import {
9
10
  HOME,
10
11
  END,
11
12
  } from '../constants';
13
+ import GlIntersectionObserver from '../../../utilities/intersection_observer/intersection_observer.vue';
12
14
  import GlListbox, { ITEM_SELECTOR } from './listbox.vue';
13
15
  import GlListboxItem from './listbox_item.vue';
14
16
  import GlListboxGroup from './listbox_group.vue';
@@ -25,6 +27,8 @@ describe('GlListbox', () => {
25
27
  });
26
28
  };
27
29
 
30
+ useMockIntersectionObserver();
31
+
28
32
  const findBaseDropdown = () => wrapper.findComponent(GlBaseDropdown);
29
33
  const findListContainer = () => wrapper.find('[role="listbox"]');
30
34
  const findListboxItems = (root = wrapper) => root.findAllComponents(GlListboxItem);
@@ -36,6 +40,7 @@ describe('GlListbox', () => {
36
40
  const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
37
41
  const findSRNumberOfResultsText = () => wrapper.find("[data-testid='listbox-number-of-results']");
38
42
  const findResetButton = () => wrapper.find("[data-testid='listbox-reset-button']");
43
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
39
44
 
40
45
  describe('toggle text', () => {
41
46
  describe.each`
@@ -435,4 +440,125 @@ describe('GlListbox', () => {
435
440
  expect(wrapper.vm.closeAndFocus).toHaveBeenCalled();
436
441
  });
437
442
  });
443
+
444
+ describe('when `infiniteScroll` prop is `true`', () => {
445
+ it('should throw an error when items are groups', () => {
446
+ expect(() => {
447
+ buildWrapper({
448
+ items: mockGroups,
449
+ infiniteScroll: true,
450
+ });
451
+ }).toThrow(
452
+ 'Infinite scroll does not support groups. Please set the "infiniteScroll" prop to "false"'
453
+ );
454
+ expect(wrapper).toHaveLoggedVueErrors();
455
+ });
456
+
457
+ it('renders `GlIntersectionObserver` component', () => {
458
+ buildWrapper({
459
+ headerText: 'Select assignee',
460
+ items: mockOptions,
461
+ infiniteScroll: true,
462
+ });
463
+
464
+ expect(findIntersectionObserver().exists()).toBe(true);
465
+ });
466
+
467
+ describe('when bottom of listbox is reached', () => {
468
+ it('emits `bottom-reached` event', () => {
469
+ buildWrapper({
470
+ items: mockOptions,
471
+ infiniteScroll: true,
472
+ });
473
+
474
+ findIntersectionObserver().vm.$emit('appear');
475
+
476
+ expect(wrapper.emitted('bottom-reached')).toEqual([[]]);
477
+ });
478
+ });
479
+
480
+ describe('when `loading` prop is `true`', () => {
481
+ it('does not render `GlIntersectionObserver` component', () => {
482
+ buildWrapper({
483
+ items: mockOptions,
484
+ infiniteScroll: true,
485
+ loading: true,
486
+ });
487
+
488
+ expect(findIntersectionObserver().exists()).toBe(false);
489
+ });
490
+ });
491
+
492
+ describe('when `searching` prop is `true`', () => {
493
+ it('does not render `GlIntersectionObserver` component', () => {
494
+ buildWrapper({
495
+ items: mockOptions,
496
+ infiniteScroll: true,
497
+ searching: true,
498
+ });
499
+
500
+ expect(findIntersectionObserver().exists()).toBe(false);
501
+ });
502
+ });
503
+
504
+ describe('when `infiniteScrollLoading` prop is `true`', () => {
505
+ beforeEach(() => {
506
+ buildWrapper({
507
+ items: mockOptions,
508
+ infiniteScroll: true,
509
+ infiniteScrollLoading: true,
510
+ });
511
+ });
512
+
513
+ it('shows loading icon', () => {
514
+ expect(wrapper.find('[data-testid="listbox-infinite-scroll-loader"]').exists()).toBe(true);
515
+ });
516
+
517
+ it('does not render `GlIntersectionObserver` component', () => {
518
+ expect(findIntersectionObserver().exists()).toBe(false);
519
+ });
520
+ });
521
+
522
+ describe('when `totalItems` prop is set', () => {
523
+ it('adds `aria-setsize` and `aria-posinset` attributes to listbox items', () => {
524
+ const totalItems = mockOptions.length;
525
+
526
+ buildWrapper({
527
+ items: mockOptions,
528
+ infiniteScroll: true,
529
+ totalItems,
530
+ });
531
+
532
+ findListboxItems().wrappers.forEach((listboxItem, index) => {
533
+ expect(listboxItem.attributes('aria-setsize')).toBe(totalItems.toString());
534
+ expect(listboxItem.attributes('aria-posinset')).toBe((index + 1).toString());
535
+ });
536
+ });
537
+ });
538
+
539
+ describe('when `totalItems` prop is not set', () => {
540
+ it('does not add `aria-setsize` and `aria-posinset` attributes to listbox items', () => {
541
+ buildWrapper({
542
+ items: mockOptions,
543
+ infiniteScroll: true,
544
+ });
545
+
546
+ findListboxItems().wrappers.forEach((listboxItem) => {
547
+ expect(listboxItem.attributes('aria-setsize')).toBe(undefined);
548
+ expect(listboxItem.attributes('aria-posinset')).toBe(undefined);
549
+ });
550
+ });
551
+ });
552
+ });
553
+
554
+ describe('when `infiniteScroll` prop is `false`', () => {
555
+ it('does not render `GlIntersectionObserver` component', () => {
556
+ buildWrapper({
557
+ items: mockOptions,
558
+ infiniteScroll: false,
559
+ });
560
+
561
+ expect(findIntersectionObserver().exists()).toBe(false);
562
+ });
563
+ });
438
564
  });
@@ -13,6 +13,8 @@ import {
13
13
  GlAvatar,
14
14
  } from '../../../../index';
15
15
  import { makeContainer } from '../../../../utils/story_decorators/container';
16
+ import { disableControls } from '../../../../utils/stories_utils';
17
+ import { setStoryTimeout } from '../../../../utils/test_utils';
16
18
  import readme from './listbox.md';
17
19
  import { mockOptions, mockGroups } from './mock_data';
18
20
  import { flattenedOptions } from './utils';
@@ -28,6 +30,8 @@ const generateProps = ({
28
30
  loading = defaultValue('loading'),
29
31
  searchable = defaultValue('searchable'),
30
32
  searching = defaultValue('searching'),
33
+ infiniteScroll = defaultValue('infiniteScroll'),
34
+ infiniteScrollLoading = defaultValue('infiniteScrollLoading'),
31
35
  noResultsText = defaultValue('noResultsText'),
32
36
  searchPlaceholder = defaultValue('searchPlaceholder'),
33
37
  noCaret = defaultValue('noCaret'),
@@ -51,6 +55,8 @@ const generateProps = ({
51
55
  loading,
52
56
  searchable,
53
57
  searching,
58
+ infiniteScroll,
59
+ infiniteScrollLoading,
54
60
  noResultsText,
55
61
  searchPlaceholder,
56
62
  noCaret,
@@ -77,6 +83,8 @@ const makeBindings = (overrides = {}) =>
77
83
  ':loading': 'loading',
78
84
  ':searchable': 'searchable',
79
85
  ':searching': 'searching',
86
+ ':infinite-scroll': 'infiniteScroll',
87
+ ':infinite-scroll-loading': 'infiniteScrollLoading',
80
88
  ':no-results-text': 'noResultsText',
81
89
  ':search-placeholder': 'searchPlaceholder',
82
90
  ':no-caret': 'noCaret',
@@ -534,3 +542,56 @@ SearchableGroups.args = generateProps({
534
542
  items: mockGroups,
535
543
  });
536
544
  SearchableGroups.decorators = [makeContainer({ height: '370px' })];
545
+
546
+ export const InfiniteScroll = (
547
+ args,
548
+ { argTypes: { infiniteScroll, infiniteScrollLoading, items, ...argTypes } }
549
+ ) => ({
550
+ props: Object.keys(argTypes),
551
+ components: {
552
+ GlListbox,
553
+ },
554
+ data() {
555
+ return {
556
+ selected: mockOptions[1].value,
557
+ items: mockOptions.slice(0, 10),
558
+ infiniteScrollLoading: false,
559
+ infiniteScroll: true,
560
+ };
561
+ },
562
+ mounted() {
563
+ if (this.startOpened) {
564
+ openListbox(this);
565
+ }
566
+ },
567
+ methods: {
568
+ onBottomReached() {
569
+ this.infiniteScrollLoading = true;
570
+
571
+ setStoryTimeout(() => {
572
+ this.items.push(...mockOptions.slice(10, 12));
573
+ this.infiniteScrollLoading = false;
574
+ this.infiniteScroll = false;
575
+ }, 1000);
576
+ },
577
+ },
578
+ template: template('', {
579
+ label: `<span class="gl-my-0" id="listbox-label">Select a department</span>`,
580
+ bindingOverrides: {
581
+ ':items': 'items',
582
+ ':infinite-scroll': 'infiniteScroll',
583
+ ':infinite-scroll-loading': 'infiniteScrollLoading',
584
+ ':total-items': 12,
585
+ '@bottom-reached': 'onBottomReached',
586
+ },
587
+ }),
588
+ });
589
+
590
+ InfiniteScroll.argTypes = {
591
+ ...disableControls(['infiniteScroll', 'infiniteScrollLoading', 'items']),
592
+ };
593
+ InfiniteScroll.parameters = {
594
+ storyshots: { disable: true },
595
+ };
596
+ InfiniteScroll.args = generateProps();
597
+ InfiniteScroll.decorators = [makeContainer({ height: '370px' })];
@@ -17,6 +17,7 @@ import {
17
17
  } from '../../../../utils/constants';
18
18
  import GlButton from '../../button/button.vue';
19
19
  import GlLoadingIcon from '../../loading_icon/loading_icon.vue';
20
+ import GlIntersectionObserver from '../../../utilities/intersection_observer/intersection_observer.vue';
20
21
  import GlSearchBoxByType from '../../search_box_by_type/search_box_by_type.vue';
21
22
  import GlBaseDropdown from '../base_dropdown/base_dropdown.vue';
22
23
  import GlListboxItem from './listbox_item.vue';
@@ -43,6 +44,7 @@ export default {
43
44
  GlSearchBoxByType,
44
45
  GlListboxSearchInput,
45
46
  GlLoadingIcon,
47
+ GlIntersectionObserver,
46
48
  },
47
49
  model: {
48
50
  prop: 'selected',
@@ -215,6 +217,37 @@ export default {
215
217
  required: false,
216
218
  default: false,
217
219
  },
220
+ /**
221
+ * Enables infinite scroll.
222
+ * When set to `true`, the `@bottom-reached` event will be fired when
223
+ * the bottom of the listbox is scrolled to.
224
+ * Does not support groups.
225
+ */
226
+ infiniteScroll: {
227
+ type: Boolean,
228
+ required: false,
229
+ default: false,
230
+ },
231
+ /**
232
+ * This prop is used for infinite scroll.
233
+ * It represents the total number of items that exist,
234
+ * even if they have not yet been loaded.
235
+ * Do not set this prop if the total number of items is unknown.
236
+ */
237
+ totalItems: {
238
+ type: Number,
239
+ required: false,
240
+ default: null,
241
+ },
242
+ /**
243
+ * This prop is used for infinite scroll.
244
+ * Set to `true` when more items are being loaded.
245
+ */
246
+ infiniteScrollLoading: {
247
+ type: Boolean,
248
+ required: false,
249
+ default: false,
250
+ },
218
251
  /**
219
252
  * Message to be displayed when filtering produced no results
220
253
  */
@@ -298,6 +331,9 @@ export default {
298
331
  }
299
332
  return Boolean(this.selected);
300
333
  },
334
+ showIntersectionObserver() {
335
+ return this.infiniteScroll && !this.infiniteScrollLoading && !this.loading && !this.searching;
336
+ },
301
337
  },
302
338
  watch: {
303
339
  selected: {
@@ -324,6 +360,17 @@ export default {
324
360
  },
325
361
  },
326
362
  },
363
+ created() {
364
+ if (
365
+ process.env.NODE_ENV !== 'production' &&
366
+ this.infiniteScroll &&
367
+ this.items.some((item) => !isOption(item))
368
+ ) {
369
+ throw new Error(
370
+ 'Infinite scroll does not support groups. Please set the "infiniteScroll" prop to "false"'
371
+ );
372
+ }
373
+ },
327
374
  methods: {
328
375
  open() {
329
376
  this.$refs.baseDropdown.open();
@@ -467,6 +514,25 @@ export default {
467
514
  closeAndFocus() {
468
515
  this.$refs.baseDropdown.closeAndFocus();
469
516
  },
517
+ onIntersectionObserverAppear() {
518
+ /**
519
+ * Emitted when bottom of listbox has been scrolled to.
520
+ * Used for infinite scroll.
521
+ *
522
+ * @event bottom-reached
523
+ */
524
+ this.$emit('bottom-reached');
525
+ },
526
+ listboxItemMoreItemsAriaAttributes(index) {
527
+ if (this.totalItems === null) {
528
+ return {};
529
+ }
530
+
531
+ return {
532
+ 'aria-setsize': this.totalItems,
533
+ 'aria-posinset': index + 1,
534
+ };
535
+ },
470
536
  isOption,
471
537
  },
472
538
  };
@@ -551,6 +617,7 @@ export default {
551
617
  :is-selected="isSelected(item)"
552
618
  :is-focused="isFocused(item)"
553
619
  :is-check-centered="isCheckCentered"
620
+ v-bind="listboxItemMoreItemsAriaAttributes(index)"
554
621
  @select="onSelect(item, $event)"
555
622
  >
556
623
  <!-- @slot Custom template of the listbox item -->
@@ -583,8 +650,17 @@ export default {
583
650
  </gl-listbox-group>
584
651
  </template>
585
652
  </template>
653
+ <gl-intersection-observer
654
+ v-if="showIntersectionObserver"
655
+ @appear="onIntersectionObserverAppear"
656
+ />
586
657
  </component>
587
-
658
+ <gl-loading-icon
659
+ v-if="infiniteScrollLoading"
660
+ data-testid="listbox-infinite-scroll-loader"
661
+ size="md"
662
+ class="gl-my-3"
663
+ />
588
664
  <span
589
665
  v-if="announceSRSearchResults"
590
666
  data-testid="listbox-number-of-results"
@@ -462,7 +462,6 @@ $gl-dropdown-width-wide: px-to-rem(400px);
462
462
  $gl-max-dropdown-max-height: px-to-rem(312px);
463
463
 
464
464
  // Broadcast messages
465
- $gl-broadcast-message-text-max-width: px-to-rem(934px);
466
465
  $gl-broadcast-message-notification-max-width: px-to-rem(300px);
467
466
 
468
467
  // Modal Widths