@gitlab/ui 63.0.0 → 63.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "63.0.0",
3
+ "version": "63.1.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -32,6 +32,7 @@
32
32
  }
33
33
 
34
34
  .gl-new-dropdown-contents {
35
+ @include gl-relative;
35
36
  @include gl-flex-grow-1;
36
37
  @include gl-overflow-y-auto;
37
38
  @include gl-pl-0;
@@ -101,4 +102,69 @@
101
102
  @include gl-mx-0;
102
103
  }
103
104
  }
105
+
106
+ $dropdown-content-padding: 0.25rem;
107
+
108
+ // Scrim overlay related styles (the shadow that appears above and below the content) indicating that the content is scrollable
109
+ .gl-new-dropdown-contents-with-scrim-overlay {
110
+ padding: 0;
111
+ }
112
+
113
+ .gl-new-dropdown-contents {
114
+ .top-scrim-wrapper,
115
+ .bottom-scrim-wrapper {
116
+ height: $dropdown-content-padding;
117
+ opacity: 0;
118
+ position: sticky;
119
+ z-index: 1;
120
+ display: block;
121
+ overflow: visible;
122
+ left: 1px;
123
+ right: 1px;
124
+ pointer-events: none;
125
+ transition: opacity 0.1s;
126
+ }
127
+
128
+ .top-scrim-wrapper {
129
+ top: 0;
130
+
131
+ .top-scrim {
132
+ left: 0;
133
+ right: 0;
134
+
135
+ &.top-scrim-light {
136
+ height: 2.25rem;
137
+ border-radius: 0.375rem 0.375rem 0 0;
138
+ background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0));
139
+ }
140
+
141
+ &.top-scrim-dark {
142
+ height: 0.25rem;
143
+ background: linear-gradient(180deg, rgba(0, 0, 0, 0.16) 0%, rgba(255, 255, 255, 0) 100%);
144
+ }
145
+ }
146
+ }
147
+
148
+ .bottom-scrim-wrapper {
149
+ bottom: 0;
150
+ height: $dropdown-content-padding;
151
+
152
+ .bottom-scrim {
153
+ height: 0;
154
+ position: relative;
155
+ top: calc(-2.25rem + #{$dropdown-content-padding});
156
+ border-radius: 0 0 0.375rem 0.375rem;
157
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1));
158
+ }
159
+ }
160
+
161
+ &.top-scrim-visible .top-scrim-wrapper,
162
+ &.bottom-scrim-visible .bottom-scrim-wrapper {
163
+ opacity: 1;
164
+ }
165
+
166
+ &.bottom-scrim-visible .bottom-scrim {
167
+ height: 2.25rem;
168
+ }
169
+ }
104
170
  }
@@ -3,11 +3,16 @@
3
3
  @include gl-px-2;
4
4
  @include gl-my-1;
5
5
 
6
- &:first-child {
6
+ /* when there is a scrim `li` items inside the list, the first and last real items
7
+ do not match the selector `:first-child` and `:last-child`,
8
+ that's why we have to target them with a different selector */
9
+ &:first-child,
10
+ .gl-new-dropdown-contents-with-scrim-overlay > &:nth-child(3):not(:last-child) {
7
11
  @include gl-mt-0;
8
12
  }
9
13
 
10
- &:last-child {
14
+ &:last-child,
15
+ .gl-new-dropdown-contents-with-scrim-overlay > &:nth-last-child(3):not(:first-child) {
11
16
  @include gl-mb-0;
12
17
  }
13
18
 
@@ -314,6 +314,8 @@ export default {
314
314
  listboxId: uniqueId('listbox-'),
315
315
  nextFocusedItemIndex: null,
316
316
  searchStr: '',
317
+ topBoundaryVisible: true,
318
+ bottomBoundaryVisible: true,
317
319
  };
318
320
  },
319
321
  computed: {
@@ -321,6 +323,15 @@ export default {
321
323
  if (this.items.length === 0 || isOption(this.items[0])) return 'ul';
322
324
  return 'div';
323
325
  },
326
+ listboxClasses() {
327
+ return {
328
+ 'top-scrim-visible': !this.topBoundaryVisible,
329
+ 'bottom-scrim-visible': !this.bottomBoundaryVisible,
330
+ };
331
+ },
332
+ itemTag() {
333
+ return this.listboxTag === 'ul' ? 'li' : 'div';
334
+ },
324
335
  flattenedOptions() {
325
336
  return flattenedOptions(this.items);
326
337
  },
@@ -379,6 +390,12 @@ export default {
379
390
  }
380
391
  return toggleClasses;
381
392
  },
393
+ hasHeader() {
394
+ return this.headerText || this.searchable;
395
+ },
396
+ hasFooter() {
397
+ return Boolean(this.$scopedSlots.footer);
398
+ },
382
399
  },
383
400
  watch: {
384
401
  selected: {
@@ -394,6 +411,15 @@ export default {
394
411
  }
395
412
  },
396
413
  },
414
+ items: {
415
+ handler() {
416
+ this.$nextTick(() => {
417
+ /* Every time the list of items changes (on search),
418
+ * the observed elements are recreated, thus we need to start obesrving them again */
419
+ this.observeScroll();
420
+ });
421
+ },
422
+ },
397
423
  ...(process.env.NODE_ENV !== 'production'
398
424
  ? {
399
425
  resetButtonLabel: {
@@ -419,6 +445,12 @@ export default {
419
445
  }
420
446
  : {}),
421
447
  },
448
+ mounted() {
449
+ this.observeScroll();
450
+ },
451
+ beforeDestroy() {
452
+ this.scrollObserver?.disconnect();
453
+ },
422
454
  methods: {
423
455
  open() {
424
456
  this.$refs.baseDropdown.open();
@@ -581,6 +613,35 @@ export default {
581
613
  'aria-posinset': index + 1,
582
614
  };
583
615
  },
616
+ observeScroll() {
617
+ const root = this.$refs.list;
618
+
619
+ const options = {
620
+ rootMargin: '8px',
621
+ root,
622
+ threshold: 1.0,
623
+ };
624
+
625
+ this.scrollObserver?.disconnect();
626
+
627
+ const observer = new IntersectionObserver((entries) => {
628
+ entries.forEach((entry) => {
629
+ this[entry.target?.$__visibilityProp] = entry.isIntersecting;
630
+ });
631
+ }, options);
632
+
633
+ const topBoundary = this.$refs['top-boundary'];
634
+ const bottomBoundary = this.$refs['bottom-boundary'];
635
+ if (topBoundary) {
636
+ topBoundary.$__visibilityProp = 'topBoundaryVisible';
637
+ observer.observe(topBoundary);
638
+ }
639
+ if (bottomBoundary) {
640
+ bottomBoundary.$__visibilityProp = 'bottomBoundaryVisible';
641
+ observer.observe(bottomBoundary);
642
+ }
643
+ this.scrollObserver = observer;
644
+ },
584
645
  isOption,
585
646
  },
586
647
  };
@@ -663,10 +724,18 @@ export default {
663
724
  ref="list"
664
725
  :aria-labelledby="listAriaLabelledBy || headerId || toggleId"
665
726
  role="listbox"
666
- class="gl-new-dropdown-contents"
727
+ class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay"
728
+ :class="listboxClasses"
667
729
  tabindex="-1"
668
730
  @keydown="onKeydown"
669
731
  >
732
+ <component :is="itemTag" class="top-scrim-wrapper" aria-hidden="true" data-testid="top-scrim">
733
+ <div
734
+ class="top-scrim"
735
+ :class="{ 'top-scrim-light': !hasHeader, 'top-scrim-dark': hasHeader }"
736
+ ></div>
737
+ </component>
738
+ <component :is="itemTag" ref="top-boundary" aria-hidden="true" />
670
739
  <template v-for="(item, index) in items">
671
740
  <template v-if="isOption(item)">
672
741
  <gl-listbox-item
@@ -714,13 +783,22 @@ export default {
714
783
  </gl-listbox-group>
715
784
  </template>
716
785
  </template>
717
- <component :is="listboxTag === 'ul' ? 'li' : 'div'" v-if="infiniteScrollLoading">
786
+ <component :is="itemTag" v-if="infiniteScrollLoading">
718
787
  <gl-loading-icon data-testid="listbox-infinite-scroll-loader" size="md" class="gl-my-3" />
719
788
  </component>
720
789
  <gl-intersection-observer
721
790
  v-if="showIntersectionObserver"
722
791
  @appear="onIntersectionObserverAppear"
723
792
  />
793
+ <component :is="itemTag" ref="bottom-boundary" aria-hidden="true" />
794
+ <component
795
+ :is="itemTag"
796
+ class="bottom-scrim-wrapper"
797
+ aria-hidden="true"
798
+ data-testid="bottom-scrim"
799
+ >
800
+ <div class="bottom-scrim" :class="{ 'gl-rounded-0!': hasFooter }"></div>
801
+ </component>
724
802
  </component>
725
803
  <span
726
804
  v-if="announceSRSearchResults"