@gitlab/ui 63.2.1 → 63.4.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.
@@ -20,4 +20,10 @@ const SERIES_NAME = {
20
20
  [SERIES_NAME_LONG_WITHOUT_SPACES]: 'Series_name_long._Lorem_ipsum_dolor_sit_amet,_consectetur_adipiscing_elit._Sed_tincidunt_interdum_sapien_ut_blandit._Nulla_fermentum_nisi_id_euismod_vulputate._END'
21
21
  };
22
22
 
23
- export { ARG_TYPE_SUBCATEGORY_ACCESSIBILITY, ARG_TYPE_SUBCATEGORY_INFINITE_SCROLL, ARG_TYPE_SUBCATEGORY_LOOK_AND_FEEL, ARG_TYPE_SUBCATEGORY_SEARCH, ARG_TYPE_SUBCATEGORY_STATE, SERIES_NAME, SERIES_NAME_LONG, SERIES_NAME_LONG_WITHOUT_SPACES, SERIES_NAME_SHORT };
23
+ /**
24
+ * Reused constants for ListBox
25
+ */
26
+
27
+ const LISTBOX_CONTAINER_HEIGHT = '370px';
28
+
29
+ export { ARG_TYPE_SUBCATEGORY_ACCESSIBILITY, ARG_TYPE_SUBCATEGORY_INFINITE_SCROLL, ARG_TYPE_SUBCATEGORY_LOOK_AND_FEEL, ARG_TYPE_SUBCATEGORY_SEARCH, ARG_TYPE_SUBCATEGORY_STATE, LISTBOX_CONTAINER_HEIGHT, SERIES_NAME, SERIES_NAME_LONG, SERIES_NAME_LONG_WITHOUT_SPACES, SERIES_NAME_SHORT };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "63.2.1",
3
+ "version": "63.4.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -41,6 +41,7 @@ describe('GlCollapsibleListbox', () => {
41
41
  const findLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
42
42
  const findSRNumberOfResultsText = () => wrapper.find("[data-testid='listbox-number-of-results']");
43
43
  const findResetButton = () => wrapper.find("[data-testid='listbox-reset-button']");
44
+ const findSelectAllButton = () => wrapper.find("[data-testid='listbox-select-all-button']");
44
45
  const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
45
46
 
46
47
  it('passes custom popper.js options to the base dropdown', () => {
@@ -514,6 +515,68 @@ describe('GlCollapsibleListbox', () => {
514
515
  });
515
516
  });
516
517
 
518
+ describe('with select all action', () => {
519
+ it('throws an error when enabling the select action without a header', () => {
520
+ expect(() => {
521
+ buildWrapper({ showSelectAllButtonLabel: 'Select All' });
522
+ }).toThrow(
523
+ 'The select all button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the showSelectAllButtonLabel prop.'
524
+ );
525
+ expect(wrapper).toHaveLoggedVueErrors();
526
+ });
527
+
528
+ it.each`
529
+ multiple | expectedResult
530
+ ${false} | ${false}
531
+ ${true} | ${true}
532
+ `(
533
+ 'shows the select all button if the label is provided and the selection is empty and multiple option is $multiple',
534
+ ({ multiple, expectedResult }) => {
535
+ buildWrapper({
536
+ headerText: 'Select assignee',
537
+ resetButtonLabel: 'Unassign',
538
+ showSelectAllButtonLabel: 'Select All',
539
+ selected: [],
540
+ items: mockOptions,
541
+ multiple,
542
+ });
543
+
544
+ expect(findResetButton().exists()).toBe(!expectedResult);
545
+ expect(findSelectAllButton().exists()).toBe(expectedResult);
546
+ }
547
+ );
548
+
549
+ it('has the label text "Select All" if the label is provided and the selection is empty', () => {
550
+ buildWrapper({
551
+ headerText: 'Select assignee',
552
+ resetButtonLabel: 'Unassign',
553
+ showSelectAllButtonLabel: 'Select All',
554
+ selected: [],
555
+ items: mockOptions,
556
+ multiple: true,
557
+ });
558
+
559
+ expect(findSelectAllButton().text()).toBe('Select All');
560
+ });
561
+
562
+ it('on click, emits the select-all event and calls closeAndFocus()', () => {
563
+ buildWrapper({
564
+ headerText: 'Select assignee',
565
+ resetButtonLabel: 'Unassign',
566
+ showSelectAllButtonLabel: 'Select All',
567
+ selected: [],
568
+ items: mockOptions,
569
+ multiple: true,
570
+ });
571
+
572
+ expect(wrapper.emitted('select-all')).toBe(undefined);
573
+
574
+ findSelectAllButton().trigger('click');
575
+
576
+ expect(wrapper.emitted('select-all')).toHaveLength(1);
577
+ });
578
+ });
579
+
517
580
  describe('when `infiniteScroll` prop is `true`', () => {
518
581
  it('should throw an error when items are groups', () => {
519
582
  expect(() => {
@@ -20,6 +20,7 @@ import {
20
20
  ARG_TYPE_SUBCATEGORY_SEARCH,
21
21
  ARG_TYPE_SUBCATEGORY_ACCESSIBILITY,
22
22
  ARG_TYPE_SUBCATEGORY_INFINITE_SCROLL,
23
+ LISTBOX_CONTAINER_HEIGHT,
23
24
  } from '../../../../utils/stories_constants';
24
25
  import { POSITION } from '../../../utilities/truncate/constants';
25
26
  import readme from './listbox.md';
@@ -55,6 +56,7 @@ const generateProps = ({
55
56
  toggleAriaLabelledBy,
56
57
  listAriaLabelledBy,
57
58
  resetButtonLabel = defaultValue('resetButtonLabel'),
59
+ showSelectAllButtonLabel = defaultValue('showSelectAllButtonLabel'),
58
60
  startOpened = true,
59
61
  fluidWidth,
60
62
  } = {}) => ({
@@ -83,6 +85,7 @@ const generateProps = ({
83
85
  toggleAriaLabelledBy,
84
86
  listAriaLabelledBy,
85
87
  resetButtonLabel,
88
+ showSelectAllButtonLabel,
86
89
  startOpened,
87
90
  fluidWidth,
88
91
  });
@@ -114,6 +117,7 @@ const makeBindings = (overrides = {}) =>
114
117
  ':toggle-aria-labelled-by': 'toggleAriaLabelledBy',
115
118
  ':list-aria-labelled-by': 'listAriaLabelledBy',
116
119
  ':reset-button-label': 'resetButtonLabel',
120
+ ':show-select-all-button-label': 'showSelectAllButtonLabel',
117
121
  ':fluid-width': 'fluidWidth',
118
122
  ...overrides,
119
123
  })
@@ -160,7 +164,7 @@ export const Default = (args, { argTypes }) => ({
160
164
  }),
161
165
  });
162
166
  Default.args = generateProps({ toggleAriaLabelledBy: 'listbox-label' });
163
- Default.decorators = [makeContainer({ height: '370px' })];
167
+ Default.decorators = [makeContainer({ height: LISTBOX_CONTAINER_HEIGHT })];
164
168
 
165
169
  export const HeaderAndFooter = (args, { argTypes }) => ({
166
170
  props: Object.keys(argTypes),
@@ -181,7 +185,7 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
181
185
  }
182
186
  },
183
187
  methods: {
184
- selectAll() {
188
+ selectAllItems() {
185
189
  const allValues = mockOptions.map(({ value }) => value);
186
190
  this.selected = [...allValues];
187
191
  },
@@ -193,7 +197,7 @@ export const HeaderAndFooter = (args, { argTypes }) => ({
193
197
  `
194
198
  <template #footer>
195
199
  <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!">
196
- <gl-button @click="selectAll" category="tertiary" block class="gl-justify-content-start! gl-mt-2!"">
200
+ <gl-button @click="selectAllItems" category="tertiary" block class="gl-justify-content-start! gl-mt-2!"">
197
201
  Select all
198
202
  </gl-button>
199
203
  <gl-button category="tertiary" block class="gl-justify-content-start! gl-mt-2!">
@@ -216,7 +220,56 @@ HeaderAndFooter.args = generateProps({
216
220
  multiple: true,
217
221
  block: true,
218
222
  });
219
- HeaderAndFooter.decorators = [makeContainer({ height: '370px' })];
223
+ HeaderAndFooter.decorators = [makeContainer({ height: LISTBOX_CONTAINER_HEIGHT })];
224
+
225
+ export const HeaderActions = (args, { argTypes }) => ({
226
+ props: Object.keys(argTypes),
227
+ components: {
228
+ GlCollapsibleListbox,
229
+ GlSearchBoxByType,
230
+ GlButtonGroup,
231
+ GlButton,
232
+ },
233
+ data() {
234
+ return {
235
+ selected: [],
236
+ };
237
+ },
238
+ computed: {
239
+ allValues() {
240
+ return mockOptions.map(({ value }) => value);
241
+ },
242
+ },
243
+ mounted() {
244
+ if (this.startOpened) {
245
+ openListbox(this);
246
+ }
247
+ },
248
+ methods: {
249
+ selectAllItems() {
250
+ this.selected = [...this.allValues];
251
+ },
252
+ onReset() {
253
+ this.selected = [];
254
+ },
255
+ },
256
+ template: template('', {
257
+ bindingOverrides: {
258
+ '@reset': 'onReset',
259
+ '@select-all': 'selectAllItems',
260
+ },
261
+ }),
262
+ });
263
+
264
+ HeaderActions.args = generateProps({
265
+ toggleText: 'Header actions',
266
+ headerText: 'Assign to department',
267
+ resetButtonLabel: 'Unassign',
268
+ showSelectAllButtonLabel: 'Select All',
269
+ multiple: true,
270
+ block: true,
271
+ });
272
+ HeaderActions.decorators = [makeContainer({ height: LISTBOX_CONTAINER_HEIGHT })];
220
273
 
221
274
  export const CustomListItem = (args, { argTypes }) => ({
222
275
  props: Object.keys(argTypes),
@@ -624,7 +677,7 @@ Searchable.args = generateProps({
624
677
  searchable: true,
625
678
  searchPlaceholder: 'Find department',
626
679
  });
627
- Searchable.decorators = [makeContainer({ height: '370px' })];
680
+ Searchable.decorators = [makeContainer({ height: LISTBOX_CONTAINER_HEIGHT })];
628
681
 
629
682
  export const SearchableGroups = (args, { argTypes }) => ({
630
683
  props: Object.keys(argTypes),
@@ -712,7 +765,7 @@ SearchableGroups.args = generateProps({
712
765
  searchable: true,
713
766
  items: mockGroups,
714
767
  });
715
- SearchableGroups.decorators = [makeContainer({ height: '370px' })];
768
+ SearchableGroups.decorators = [makeContainer({ height: LISTBOX_CONTAINER_HEIGHT })];
716
769
 
717
770
  export const InfiniteScroll = (
718
771
  args,
@@ -764,7 +817,7 @@ InfiniteScroll.parameters = {
764
817
  storyshots: { disable: true },
765
818
  };
766
819
  InfiniteScroll.args = generateProps();
767
- InfiniteScroll.decorators = [makeContainer({ height: '370px' })];
820
+ InfiniteScroll.decorators = [makeContainer({ height: LISTBOX_CONTAINER_HEIGHT })];
768
821
 
769
822
  export const WithLongContent = (args, { argTypes: { items, ...argTypes } }) => ({
770
823
  props: Object.keys(argTypes),
@@ -280,6 +280,17 @@ export default {
280
280
  required: false,
281
281
  default: '',
282
282
  },
283
+ /**
284
+ * The select all button's label, to be rendered in the header. If this is omitted, the button is not
285
+ * rendered.
286
+ * The select all button requires a header to be set, so this prop should be used in conjunction with
287
+ * headerText.
288
+ */
289
+ showSelectAllButtonLabel: {
290
+ type: String,
291
+ required: false,
292
+ default: '',
293
+ },
283
294
  /**
284
295
  * Render the toggle button as a block element
285
296
  */
@@ -373,6 +384,17 @@ export default {
373
384
  }
374
385
  return Boolean(this.selected);
375
386
  },
387
+ showSelectAllButton() {
388
+ if (!this.showSelectAllButtonLabel) {
389
+ return false;
390
+ }
391
+
392
+ if (!this.multiple) {
393
+ return false;
394
+ }
395
+
396
+ return this.selected.length === 0;
397
+ },
376
398
  showIntersectionObserver() {
377
399
  return this.infiniteScroll && !this.infiniteScrollLoading && !this.loading && !this.searching;
378
400
  },
@@ -432,6 +454,16 @@ export default {
432
454
  }
433
455
  },
434
456
  },
457
+ showSelectAllButtonLabel: {
458
+ immediate: true,
459
+ handler(showSelectAllButtonLabel) {
460
+ if (showSelectAllButtonLabel && !this.headerText) {
461
+ throw new Error(
462
+ 'The select all button cannot be rendered without a header. Either provide a header via the headerText prop, or do not provide the showSelectAllButtonLabel prop.'
463
+ );
464
+ }
465
+ },
466
+ },
435
467
  infiniteScroll: {
436
468
  immediate: true,
437
469
  handler(newValue) {
@@ -591,6 +623,14 @@ export default {
591
623
  this.$emit('reset');
592
624
  this.closeAndFocus();
593
625
  },
626
+ onSelectAllButtonClicked() {
627
+ /**
628
+ * Emitted when the select all button is clicked
629
+ *
630
+ * @event select-all
631
+ */
632
+ this.$emit('select-all');
633
+ },
594
634
  closeAndFocus() {
595
635
  this.$refs.baseDropdown.closeAndFocus();
596
636
  },
@@ -690,12 +730,21 @@ export default {
690
730
  <gl-button
691
731
  v-if="showResetButton"
692
732
  category="tertiary"
693
- class="gl-focus-inset-border-2-blue-400! gl-flex-shrink-0 gl-font-sm! gl-px-2! gl-py-2! gl-w-auto! gl-m-0!"
733
+ class="gl-focus-inset-border-2-blue-400! gl-flex-shrink-0 gl-font-sm! gl-px-2! gl-py-2! gl-w-auto! gl-m-0! gl-max-w-50p gl-text-overflow-ellipsis"
694
734
  data-testid="listbox-reset-button"
695
735
  @click="onResetButtonClicked"
696
736
  >
697
737
  {{ resetButtonLabel }}
698
738
  </gl-button>
739
+ <gl-button
740
+ v-if="showSelectAllButton"
741
+ category="tertiary"
742
+ class="gl-focus-inset-border-2-blue-400! gl-flex-shrink-0 gl-font-sm! gl-px-2! gl-py-2! gl-w-auto! gl-m-0! gl-max-w-50p gl-text-overflow-ellipsis"
743
+ data-testid="listbox-select-all-button"
744
+ @click="onSelectAllButtonClicked"
745
+ >
746
+ {{ showSelectAllButtonLabel }}
747
+ </gl-button>
699
748
  </div>
700
749
 
701
750
  <div v-if="searchable" :class="$options.HEADER_ITEMS_BORDER_CLASSES">
@@ -6745,18 +6745,90 @@
6745
6745
  .gl-gap-7\! {
6746
6746
  gap: $gl-spacing-scale-7 !important;
6747
6747
  }
6748
+ .gl-column-gap-1 {
6749
+ column-gap: $gl-spacing-scale-1;
6750
+ }
6751
+ .gl-column-gap-1\! {
6752
+ column-gap: $gl-spacing-scale-1 !important;
6753
+ }
6754
+ .gl-column-gap-2 {
6755
+ column-gap: $gl-spacing-scale-2;
6756
+ }
6757
+ .gl-column-gap-2\! {
6758
+ column-gap: $gl-spacing-scale-2 !important;
6759
+ }
6760
+ .gl-column-gap-3 {
6761
+ column-gap: $gl-spacing-scale-3;
6762
+ }
6763
+ .gl-column-gap-3\! {
6764
+ column-gap: $gl-spacing-scale-3 !important;
6765
+ }
6766
+ .gl-column-gap-4 {
6767
+ column-gap: $gl-spacing-scale-4;
6768
+ }
6769
+ .gl-column-gap-4\! {
6770
+ column-gap: $gl-spacing-scale-4 !important;
6771
+ }
6772
+ .gl-column-gap-5 {
6773
+ column-gap: $gl-spacing-scale-5;
6774
+ }
6775
+ .gl-column-gap-5\! {
6776
+ column-gap: $gl-spacing-scale-5 !important;
6777
+ }
6748
6778
  .gl-column-gap-6 {
6749
6779
  column-gap: $gl-spacing-scale-6;
6750
6780
  }
6751
6781
  .gl-column-gap-6\! {
6752
6782
  column-gap: $gl-spacing-scale-6 !important;
6753
6783
  }
6784
+ .gl-column-gap-7 {
6785
+ column-gap: $gl-spacing-scale-7;
6786
+ }
6787
+ .gl-column-gap-7\! {
6788
+ column-gap: $gl-spacing-scale-7 !important;
6789
+ }
6790
+ .gl-row-gap-1 {
6791
+ row-gap: $gl-spacing-scale-1;
6792
+ }
6793
+ .gl-row-gap-1\! {
6794
+ row-gap: $gl-spacing-scale-1 !important;
6795
+ }
6796
+ .gl-row-gap-2 {
6797
+ row-gap: $gl-spacing-scale-2;
6798
+ }
6799
+ .gl-row-gap-2\! {
6800
+ row-gap: $gl-spacing-scale-2 !important;
6801
+ }
6802
+ .gl-row-gap-3 {
6803
+ row-gap: $gl-spacing-scale-3;
6804
+ }
6805
+ .gl-row-gap-3\! {
6806
+ row-gap: $gl-spacing-scale-3 !important;
6807
+ }
6808
+ .gl-row-gap-4 {
6809
+ row-gap: $gl-spacing-scale-4;
6810
+ }
6811
+ .gl-row-gap-4\! {
6812
+ row-gap: $gl-spacing-scale-4 !important;
6813
+ }
6814
+ .gl-row-gap-5 {
6815
+ row-gap: $gl-spacing-scale-5;
6816
+ }
6817
+ .gl-row-gap-5\! {
6818
+ row-gap: $gl-spacing-scale-5 !important;
6819
+ }
6754
6820
  .gl-row-gap-6 {
6755
6821
  row-gap: $gl-spacing-scale-6;
6756
6822
  }
6757
6823
  .gl-row-gap-6\! {
6758
6824
  row-gap: $gl-spacing-scale-6 !important;
6759
6825
  }
6826
+ .gl-row-gap-7 {
6827
+ row-gap: $gl-spacing-scale-7;
6828
+ }
6829
+ .gl-row-gap-7\! {
6830
+ row-gap: $gl-spacing-scale-7 !important;
6831
+ }
6760
6832
  .gl-xs-mb-3 {
6761
6833
  @include gl-media-breakpoint-down(sm) {
6762
6834
  margin-bottom: $gl-spacing-scale-3;
@@ -886,10 +886,34 @@
886
886
  * - Utilities should strictly follow $gl-spacing-scale
887
887
  */
888
888
 
889
+ @mixin gl-column-gap-1 {
890
+ column-gap: $gl-spacing-scale-1;
891
+ }
892
+
893
+ @mixin gl-column-gap-2 {
894
+ column-gap: $gl-spacing-scale-2;
895
+ }
896
+
897
+ @mixin gl-column-gap-3 {
898
+ column-gap: $gl-spacing-scale-3;
899
+ }
900
+
901
+ @mixin gl-column-gap-4 {
902
+ column-gap: $gl-spacing-scale-4;
903
+ }
904
+
905
+ @mixin gl-column-gap-5 {
906
+ column-gap: $gl-spacing-scale-5;
907
+ }
908
+
889
909
  @mixin gl-column-gap-6 {
890
910
  column-gap: $gl-spacing-scale-6;
891
911
  }
892
912
 
913
+ @mixin gl-column-gap-7 {
914
+ column-gap: $gl-spacing-scale-7;
915
+ }
916
+
893
917
  /**
894
918
  * Row gap utilities
895
919
  *
@@ -898,10 +922,34 @@
898
922
  * - Utilities should strictly follow $gl-spacing-scale
899
923
  */
900
924
 
925
+ @mixin gl-row-gap-1 {
926
+ row-gap: $gl-spacing-scale-1;
927
+ }
928
+
929
+ @mixin gl-row-gap-2 {
930
+ row-gap: $gl-spacing-scale-2;
931
+ }
932
+
933
+ @mixin gl-row-gap-3 {
934
+ row-gap: $gl-spacing-scale-3;
935
+ }
936
+
937
+ @mixin gl-row-gap-4 {
938
+ row-gap: $gl-spacing-scale-4;
939
+ }
940
+
941
+ @mixin gl-row-gap-5 {
942
+ row-gap: $gl-spacing-scale-5;
943
+ }
944
+
901
945
  @mixin gl-row-gap-6 {
902
946
  row-gap: $gl-spacing-scale-6;
903
947
  }
904
948
 
949
+ @mixin gl-row-gap-7 {
950
+ row-gap: $gl-spacing-scale-7;
951
+ }
952
+
905
953
  /**
906
954
  * Responsive margin utilities.
907
955
  *
@@ -22,3 +22,9 @@ export const SERIES_NAME = {
22
22
  [SERIES_NAME_LONG_WITHOUT_SPACES]:
23
23
  'Series_name_long._Lorem_ipsum_dolor_sit_amet,_consectetur_adipiscing_elit._Sed_tincidunt_interdum_sapien_ut_blandit._Nulla_fermentum_nisi_id_euismod_vulputate._END',
24
24
  };
25
+
26
+ /**
27
+ * Reused constants for ListBox
28
+ */
29
+
30
+ export const LISTBOX_CONTAINER_HEIGHT = '370px';