@gitlab/ui 59.5.0 → 60.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.
Files changed (28) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/components/base/banner/banner.js +1 -1
  3. package/dist/components/base/breadcrumb/breadcrumb.js +7 -5
  4. package/dist/components/base/new_dropdowns/base_dropdown/base_dropdown.js +7 -1
  5. package/dist/components/base/new_dropdowns/disclosure/disclosure_dropdown.js +10 -1
  6. package/dist/components/base/new_dropdowns/listbox/listbox.js +10 -1
  7. package/dist/index.css +2 -2
  8. package/dist/index.css.map +1 -1
  9. package/package.json +2 -2
  10. package/scss_to_js/scss_variables.js +2 -1
  11. package/scss_to_js/scss_variables.json +8 -3
  12. package/src/components/base/banner/banner.vue +1 -1
  13. package/src/components/base/breadcrumb/breadcrumb.md +14 -13
  14. package/src/components/base/breadcrumb/breadcrumb.scss +0 -8
  15. package/src/components/base/breadcrumb/breadcrumb.spec.js +16 -13
  16. package/src/components/base/breadcrumb/breadcrumb.stories.js +5 -9
  17. package/src/components/base/breadcrumb/breadcrumb.vue +15 -7
  18. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.spec.js +9 -0
  19. package/src/components/base/new_dropdowns/base_dropdown/base_dropdown.vue +6 -0
  20. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.md +8 -0
  21. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.spec.js +7 -0
  22. package/src/components/base/new_dropdowns/disclosure/disclosure_dropdown.vue +10 -0
  23. package/src/components/base/new_dropdowns/dropdown.scss +2 -1
  24. package/src/components/base/new_dropdowns/listbox/listbox.md +8 -0
  25. package/src/components/base/new_dropdowns/listbox/listbox.spec.js +7 -0
  26. package/src/components/base/new_dropdowns/listbox/listbox.stories.js +58 -1
  27. package/src/components/base/new_dropdowns/listbox/listbox.vue +10 -0
  28. package/src/scss/variables.scss +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "59.5.0",
3
+ "version": "60.1.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -90,7 +90,7 @@
90
90
  "@gitlab/eslint-plugin": "18.3.0",
91
91
  "@gitlab/fonts": "^1.2.0",
92
92
  "@gitlab/stylelint-config": "4.1.0",
93
- "@gitlab/svgs": "3.36.0",
93
+ "@gitlab/svgs": "3.38.0",
94
94
  "@rollup/plugin-commonjs": "^11.1.0",
95
95
  "@rollup/plugin-node-resolve": "^7.1.3",
96
96
  "@rollup/plugin-replace": "^2.3.2",
@@ -331,7 +331,8 @@ export const glIconRadio = ''
331
331
  export const defaultIconSize = '1rem'
332
332
  export const glIconSizes = '8 12 14 16 24 32 48 72'
333
333
  export const glDropdownWidth = '15rem'
334
- export const glNewDropdownWidth = '16rem'
334
+ export const glNewDropdownMinWidth = '15.5rem'
335
+ export const glNewDropdownMaxWidth = '28.5rem'
335
336
  export const glDropdownWidthNarrow = '10rem'
336
337
  export const glDropdownWidthWide = '25rem'
337
338
  export const glMaxDropdownMaxHeight = '19.5rem'
@@ -1766,9 +1766,14 @@
1766
1766
  "compiledValue": "15rem"
1767
1767
  },
1768
1768
  {
1769
- "name": "$gl-new-dropdown-width",
1770
- "value": "px-to-rem(256px)",
1771
- "compiledValue": "16rem"
1769
+ "name": "$gl-new-dropdown-min-width",
1770
+ "value": "px-to-rem(248px)",
1771
+ "compiledValue": "15.5rem"
1772
+ },
1773
+ {
1774
+ "name": "$gl-new-dropdown-max-width",
1775
+ "value": "px-to-rem(456px)",
1776
+ "compiledValue": "28.5rem"
1772
1777
  },
1773
1778
  {
1774
1779
  "name": "$gl-dropdown-width-narrow",
@@ -95,7 +95,7 @@ export default {
95
95
  body-class="gl-display-flex gl-p-0!"
96
96
  >
97
97
  <div v-if="svgPath" class="gl-banner-illustration">
98
- <img :src="svgPath" alt="" role="presentation" />
98
+ <img :src="svgPath" alt="" />
99
99
  </div>
100
100
  <div class="gl-banner-content">
101
101
  <h2 class="gl-banner-title">{{ title }}</h2>
@@ -1,22 +1,23 @@
1
1
  ## Usage
2
2
 
3
- This component provides a `<slot #avatar>` so an avatar can appear before the first breadcrumb.
3
+ This component also allows for optional avatars on each item.
4
+
5
+ `avatarPath` should passed along with `text` and `href` in `items`.
6
+ Here is an example of how an item with an avatar should look:
4
7
 
5
8
  **note:** the component supports passing the property `to` in the list items to enable navigation
6
9
  through `vue-router`
7
10
 
8
11
  ### Example
9
12
 
10
- ```html
11
- <gl-breadcrumb :items="items">
12
- <template #avatar>
13
- <img
14
- alt=""
15
- class="gl-breadcrumb-avatar-tile"
16
- src="/path/to/image.png"
17
- width="16"
18
- height="16"
19
- />
20
- </template>
21
- </gl-breadcrumb>
13
+ ```js
14
+ items = [
15
+ {
16
+ text: 'First item',
17
+ href: '#',
18
+ avatarPath: '/avatar.png',
19
+ },
20
+ ];
21
+
22
+ <gl-breadcrumb :items="items" />
22
23
  ```
@@ -16,14 +16,6 @@ $breadcrumb-max-width: $grid-size * 16;
16
16
  }
17
17
  }
18
18
 
19
- .gl-breadcrumb-avatar-tile {
20
- @include gl-mr-2;
21
- @include gl-border-1;
22
- @include gl-border-solid;
23
- @include gl-border-gray-a-08;
24
- @include gl-rounded-base;
25
- }
26
-
27
19
  // bootstrap overrides
28
20
  .gl-breadcrumb-item {
29
21
  @include gl-font-sm;
@@ -1,5 +1,7 @@
1
1
  import { shallowMount } from '@vue/test-utils';
2
2
  import { nextTick } from 'vue';
3
+ import avatarPath1 from '../../../../static/img/avatar.png';
4
+ import avatarPath3 from '../../../../static/img/avatar_1.png';
3
5
  import Breadcrumb, { COLLAPSE_AT_SIZE } from './breadcrumb.vue';
4
6
  import GlBreadcrumbItem from './breadcrumb_item.vue';
5
7
 
@@ -7,23 +9,27 @@ describe('Breadcrumb component', () => {
7
9
  let wrapper;
8
10
 
9
11
  const items = [
10
- { text: 'first_breadcrumb', href: 'http://gitlab.com' },
12
+ { text: 'first_breadcrumb', href: 'https://gitlab.com', avatarPath: avatarPath1 },
11
13
  {
12
14
  text: 'second_breadcrumb',
13
15
  to: 'to_value',
14
16
  },
15
- { text: 'third_breadcrumb', href: 'http://about.gitlab.com' },
17
+ {
18
+ text: 'third_breadcrumb',
19
+ href: 'https://about.gitlab.com',
20
+ avatarPath: avatarPath3,
21
+ },
16
22
  ];
17
23
 
18
24
  const extraItems = [
19
- { text: 'fourth_breadcrumb', href: 'http://gitlab.com' },
25
+ { text: 'fourth_breadcrumb', href: 'https://gitlab.com' },
20
26
  {
21
27
  text: 'fifth_breadcrumb',
22
28
  to: 'to_value',
23
29
  },
24
30
  ];
25
31
 
26
- const findAvatarSlot = () => wrapper.find('[data-testid="avatar-slot"]');
32
+ const findAllAvatars = () => wrapper.findAll('[data-testid="avatar"]');
27
33
  const findBreadcrumbItems = () => wrapper.findAllComponents(GlBreadcrumbItem);
28
34
  const findCollapsedListExpander = () => wrapper.find('[data-testid="collapsed-expander"]');
29
35
 
@@ -35,9 +41,6 @@ describe('Breadcrumb component', () => {
35
41
  const createComponent = (propsData = { items }) => {
36
42
  wrapper = shallowMount(Breadcrumb, {
37
43
  propsData,
38
- slots: {
39
- avatar: '<div data-testid="avatar-slot"></div>',
40
- },
41
44
  stubs: {
42
45
  GlBreadcrumbItem,
43
46
  },
@@ -50,19 +53,19 @@ describe('Breadcrumb component', () => {
50
53
  ];
51
54
  };
52
55
 
53
- describe('slots', () => {
54
- it('has an avatar slot', () => {
56
+ describe('items', () => {
57
+ it('has one breadcrumb-item for each item in the items props', () => {
55
58
  createComponent();
56
59
 
57
- expect(findAvatarSlot().exists()).toBe(true);
60
+ expect(findBreadcrumbItems()).toHaveLength(items.length);
58
61
  });
59
62
  });
60
63
 
61
- describe('items', () => {
62
- it('has one breadcrumb-item for each item in the items props', () => {
64
+ describe('avatars', () => {
65
+ it('renders 2 avatars when 2 avatarPaths are passed', () => {
63
66
  createComponent();
64
67
 
65
- expect(findBreadcrumbItems()).toHaveLength(items.length);
68
+ expect(findAllAvatars()).toHaveLength(2);
66
69
  });
67
70
  });
68
71
 
@@ -1,24 +1,19 @@
1
- import avatarPath from '../../../../static/img/avatar.png';
1
+ import avatarPath1 from '../../../../static/img/avatar_1.png';
2
+ import avatarPath2 from '../../../../static/img/avatar_2.png';
2
3
  import GlBreadcrumb from './breadcrumb.vue';
3
4
  import readme from './breadcrumb.md';
4
5
 
5
6
  const template = `
6
7
  <gl-breadcrumb
7
8
  :items="items"
8
- >
9
- <template #avatar>
10
- <img alt=""
11
- class="gl-breadcrumb-avatar-tile" src="${avatarPath}"
12
- width="16"
13
- height="16" />
14
- </template>
15
- </gl-breadcrumb>
9
+ />
16
10
  `;
17
11
 
18
12
  const defaultItems = [
19
13
  {
20
14
  text: 'First item',
21
15
  href: '#',
16
+ avatarPath: avatarPath1,
22
17
  },
23
18
  {
24
19
  text: 'Second item',
@@ -27,6 +22,7 @@ const defaultItems = [
27
22
  {
28
23
  text: 'Third item',
29
24
  href: '#',
25
+ avatarPath: avatarPath2,
30
26
  },
31
27
  {
32
28
  text: 'Fourth item',
@@ -2,6 +2,7 @@
2
2
  <script>
3
3
  import { BBreadcrumb } from 'bootstrap-vue';
4
4
  import GlButton from '../button/button.vue';
5
+ import GlAvatar from '../avatar/avatar.vue';
5
6
  import { GlTooltipDirective } from '../../../directives/tooltip';
6
7
  import GlBreadcrumbItem from './breadcrumb_item.vue';
7
8
 
@@ -12,6 +13,7 @@ export default {
12
13
  BBreadcrumb,
13
14
  GlButton,
14
15
  GlBreadcrumbItem,
16
+ GlAvatar,
15
17
  },
16
18
  directives: {
17
19
  GlTooltip: GlTooltipDirective,
@@ -25,9 +27,9 @@ export default {
25
27
  type: Array,
26
28
  required: true,
27
29
  default: () => [{ text: '', href: '' }],
28
- validator: (links) => {
29
- return links.every((link) => {
30
- const keys = Object.keys(link);
30
+ validator: (items) => {
31
+ return items.every((item) => {
32
+ const keys = Object.keys(item);
31
33
  return keys.includes('text') && (keys.includes('href') || keys.includes('to'));
32
34
  });
33
35
  },
@@ -82,8 +84,6 @@ export default {
82
84
  </script>
83
85
  <template>
84
86
  <nav class="gl-breadcrumbs" aria-label="Breadcrumb">
85
- <!-- @slot The avatar to display. -->
86
- <slot name="avatar"></slot>
87
87
  <b-breadcrumb class="gl-breadcrumb-list" v-bind="$attrs" v-on="$listeners">
88
88
  <template v-for="(item, index) in items">
89
89
  <!-- eslint-disable-next-line vue/valid-v-for (for @vue/compat) -->
@@ -94,8 +94,16 @@ export default {
94
94
  :href="item.href"
95
95
  :to="item.to"
96
96
  :aria-current="getAriaCurrentAttr(index)"
97
- >{{ item.text }}</gl-breadcrumb-item
98
- >
97
+ ><gl-avatar
98
+ v-if="item.avatarPath"
99
+ :src="item.avatarPath"
100
+ :size="16"
101
+ aria-hidden="true"
102
+ class="gl-breadcrumb-avatar-tile gl-border gl-mr-2 gl-rounded-base!"
103
+ shape="rect"
104
+ data-testid="avatar"
105
+ /><span>{{ item.text }}</span>
106
+ </gl-breadcrumb-item>
99
107
 
100
108
  <template v-if="showCollapsedBreadcrumbsExpander(index)">
101
109
  <!-- eslint-disable-next-line vue/require-v-for-key (for @vue/compat) -->
@@ -80,6 +80,15 @@ describe('base dropdown', () => {
80
80
  );
81
81
  });
82
82
 
83
+ it('should pass custom options to popper.js, overriding built-in ones', async () => {
84
+ await buildWrapper({ placement: 'right', popperOptions: { placement: 'auto-start' } });
85
+ expect(mockCreatePopper).toHaveBeenCalledWith(
86
+ findDefaultDropdownToggle().element,
87
+ findDropdownMenu().element,
88
+ { ...POPPER_CONFIG, placement: 'auto-start' }
89
+ );
90
+ });
91
+
83
92
  it('should update popper instance when component is updated', async () => {
84
93
  await buildWrapper();
85
94
  await findDefaultDropdownToggle().trigger('click');
@@ -117,6 +117,11 @@ export default {
117
117
  required: false,
118
118
  default: null,
119
119
  },
120
+ popperOptions: {
121
+ type: Object,
122
+ required: false,
123
+ default: () => ({}),
124
+ },
120
125
  },
121
126
  data() {
122
127
  return {
@@ -192,6 +197,7 @@ export default {
192
197
  return {
193
198
  placement: dropdownPlacements[this.placement],
194
199
  ...POPPER_CONFIG,
200
+ ...this.popperOptions,
195
201
  };
196
202
  },
197
203
  },
@@ -122,3 +122,11 @@ To render custom group labels, use the `group-label` scoped slot:
122
122
 
123
123
  Besides default components, disclosure dropdown can render miscellaneous content inside it.
124
124
  In this case the user is responsible for handling all events and navigation inside the disclosure.
125
+
126
+ #### Dealing with long option texts
127
+
128
+ - Some options might have long non-wrapping text that would overflow the dropdown maximum width. In
129
+ such cases, it's recommended to override the `#list-item` slot and to truncate the option text using
130
+ `GlTruncate`.
131
+ - If the toggle text reflects the selected option text, it might be necessary to truncate
132
+ it too by overriding the `#toggle` slot.
@@ -37,6 +37,13 @@ describe('GlDisclosureDropdown', () => {
37
37
 
38
38
  jest.spyOn(utils, 'filterVisible').mockImplementation((items) => items);
39
39
 
40
+ it('passes custom popper.js options to the base dropdown', () => {
41
+ const popperOptions = { foo: 'bar' };
42
+ buildWrapper({ popperOptions });
43
+
44
+ expect(findBaseDropdown().props('popperOptions')).toEqual(popperOptions);
45
+ });
46
+
40
47
  describe('toggle text', () => {
41
48
  it('should pass toggle text to the base dropdown', () => {
42
49
  const toggleText = 'Merge requests';
@@ -164,6 +164,15 @@ export default {
164
164
  required: false,
165
165
  default: null,
166
166
  },
167
+ /**
168
+ * Options to be passed to the underlying Popper.js instance.
169
+ * Overrides built-in options.
170
+ */
171
+ popperOptions: {
172
+ type: Object,
173
+ required: false,
174
+ default: () => ({}),
175
+ },
167
176
  },
168
177
  data() {
169
178
  return {
@@ -278,6 +287,7 @@ export default {
278
287
  :loading="loading"
279
288
  :no-caret="noCaret"
280
289
  :placement="placement"
290
+ :popper-options="popperOptions"
281
291
  class="gl-disclosure-dropdown"
282
292
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
283
293
  @[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
@@ -20,7 +20,8 @@
20
20
  @include gl-border-gray-200;
21
21
  @include gl-rounded-lg;
22
22
  @include gl-shadow-md;
23
- width: $gl-new-dropdown-width;
23
+ min-width: $gl-new-dropdown-min-width;
24
+ max-width: $gl-new-dropdown-max-width;
24
25
  z-index: 1000;
25
26
  }
26
27
 
@@ -141,3 +141,11 @@ Screen reader will announce this text when the list is updated.
141
141
  </template>
142
142
  </gl-collapsible-listbox>
143
143
  ```
144
+
145
+ #### Dealing with long option texts
146
+
147
+ - Some options might have long non-wrapping text that would overflow the dropdown maximum width. In
148
+ such cases, it's recommended to override the `#list-item` slot and to truncate the option text using
149
+ `GlTruncate`.
150
+ - If the toggle text reflects the selected option text, it might be necessary to truncate
151
+ it too by overriding the `#toggle` slot.
@@ -43,6 +43,13 @@ describe('GlCollapsibleListbox', () => {
43
43
  const findResetButton = () => wrapper.find("[data-testid='listbox-reset-button']");
44
44
  const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
45
45
 
46
+ it('passes custom popper.js options to the base dropdown', () => {
47
+ const popperOptions = { foo: 'bar' };
48
+ buildWrapper({ popperOptions });
49
+
50
+ expect(findBaseDropdown().props('popperOptions')).toEqual(popperOptions);
51
+ });
52
+
46
53
  describe('toggle text', () => {
47
54
  describe.each`
48
55
  toggleText | multiple | selected | expectedToggleText
@@ -10,6 +10,7 @@ import GlButtonGroup from '../../button_group/button_group.vue';
10
10
  import GlButton from '../../button/button.vue';
11
11
  import GlBadge from '../../badge/badge.vue';
12
12
  import GlAvatar from '../../avatar/avatar.vue';
13
+ import GlTruncate from '../../../utilities/truncate/truncate.vue';
13
14
  import { makeContainer } from '../../../../utils/story_decorators/container';
14
15
  import { setStoryTimeout } from '../../../../utils/test_utils';
15
16
  import { disableControls } from '../../../../utils/stories_utils';
@@ -20,6 +21,7 @@ import {
20
21
  ARG_TYPE_SUBCATEGORY_ACCESSIBILITY,
21
22
  ARG_TYPE_SUBCATEGORY_INFINITE_SCROLL,
22
23
  } from '../../../../utils/stories_constants';
24
+ import { POSITION } from '../../../utilities/truncate/constants';
23
25
  import readme from './listbox.md';
24
26
  import { mockOptions, mockGroups, mockUsers } from './mock_data';
25
27
  import { flattenedOptions } from './utils';
@@ -755,7 +757,6 @@ export const InfiniteScroll = (
755
757
  },
756
758
  }),
757
759
  });
758
-
759
760
  InfiniteScroll.argTypes = {
760
761
  ...disableControls(['infiniteScroll', 'infiniteScrollLoading', 'items']),
761
762
  };
@@ -764,3 +765,59 @@ InfiniteScroll.parameters = {
764
765
  };
765
766
  InfiniteScroll.args = generateProps();
766
767
  InfiniteScroll.decorators = [makeContainer({ height: '370px' })];
768
+
769
+ export const WithLongContent = (args, { argTypes: { items, ...argTypes } }) => ({
770
+ props: Object.keys(argTypes),
771
+ components: {
772
+ GlCollapsibleListbox,
773
+ GlButton,
774
+ GlTruncate,
775
+ },
776
+ data() {
777
+ const positions = Object.values(POSITION);
778
+ const longItems = Array.from({ length: positions.length }).map((_, index) => ({
779
+ value: `long_value_${index}`,
780
+ text: `${
781
+ index + 1
782
+ }. This is a super long option. Its text is so long that it overflows the max content width. Thankfully, we are truncating it!`,
783
+ truncatePosition: positions[index],
784
+ }));
785
+
786
+ return {
787
+ selected: longItems[0].value,
788
+ items: longItems,
789
+ };
790
+ },
791
+ mounted() {
792
+ if (this.startOpened) {
793
+ openListbox(this);
794
+ }
795
+ },
796
+ computed: {
797
+ customToggleText() {
798
+ return this.items.find(({ value }) => value === this.selected).text;
799
+ },
800
+ numberOfSearchResults() {
801
+ return this.filteredItems.length === 1 ? '1 result' : `${this.filteredItems.length} results`;
802
+ },
803
+ },
804
+ template: template(
805
+ `
806
+ <template #toggle>
807
+ <gl-button class="gl-w-30">
808
+ <gl-truncate :text="customToggleText" />
809
+ </gl-button>
810
+ </template>
811
+ <template #list-item="{ item }">
812
+ <gl-truncate :text="item.text" :position="item.truncatePosition" />
813
+ </template>
814
+ `,
815
+ {
816
+ label: `<span class="gl-my-0" id="listbox-label">Select the longest option</span>`,
817
+ bindingOverrides: {
818
+ ':items': 'items',
819
+ },
820
+ }
821
+ ),
822
+ });
823
+ WithLongContent.args = generateProps();
@@ -287,6 +287,15 @@ export default {
287
287
  required: false,
288
288
  default: false,
289
289
  },
290
+ /**
291
+ * Options to be passed to the underlying Popper.js instance.
292
+ * Overrides built-in options.
293
+ */
294
+ popperOptions: {
295
+ type: Object,
296
+ required: false,
297
+ default: () => ({}),
298
+ },
290
299
  },
291
300
  data() {
292
301
  return {
@@ -585,6 +594,7 @@ export default {
585
594
  :loading="loading"
586
595
  :no-caret="noCaret"
587
596
  :placement="placement"
597
+ :popper-options="popperOptions"
588
598
  @[$options.events.GL_DROPDOWN_SHOWN]="onShow"
589
599
  @[$options.events.GL_DROPDOWN_HIDDEN]="onHide"
590
600
  >
@@ -464,7 +464,8 @@ $gl-icon-sizes: 8 12 14 16 24 32 48 72;
464
464
 
465
465
  // Dropdowns
466
466
  $gl-dropdown-width: px-to-rem(240px);
467
- $gl-new-dropdown-width: px-to-rem(256px);
467
+ $gl-new-dropdown-min-width: px-to-rem(248px);
468
+ $gl-new-dropdown-max-width: px-to-rem(456px);
468
469
  $gl-dropdown-width-narrow: px-to-rem(160px);
469
470
  $gl-dropdown-width-wide: px-to-rem(400px);
470
471
  $gl-max-dropdown-max-height: px-to-rem(312px);